起因

如下代码,我们调用document.getElementById获取一个元素,然后尝试对其调用getContext方法。然而这段代码无法通过ts检查,而是会抛出错误。

const myCanvas = document.getElementById("canvas")
const context = myCanvas.getContext('2d');

错误1

'myCanvas' is possibly 'null'.ts(18047)

这个很好理解,错误信息告诉我们,myCanvas的值可能为null,页面上可能不存在idcanvas元素,这种情况下,document.getElementById就会返回null,当尝试对null调用方法,报错也就很合理了。

这里我们加一个if判断,就能轻松的解决这个问题:

const myCanvas = document.getElementById("canvas")

// 当myCanvas为真时(剔除了myCanvas为null的情况),我们才对其调用getContext方法。
if (myCanvas) {
    const context = myCanvas.getContext('2d')
}

错误2

myCanvas可能为null的报错解决了,我们发现ts又报了另一个错误。

Property 'getContext' does not exist on type 'HTMLElement'.ts(2339)

上面的错误信息告诉我们,HTMLElement上并不存在getContext方法,这是怎么回事呢?
实际上,标准委员会规定document.getElementById在找到元素时,会返回一个HTMLElement元素,而HTMLElement元素上是没有getContext方法的。所以在HTMLElement类型的元素上调用一个不存在的方法,ts会报错,这很合理。

解决方案就是明确的告诉ts,我们的代码会返回HTMLCanvasElement类型,因为我们知道canvas并不是一个普通的HTMLElement类型元素,而是一个HTMLCanvasElement类型元素,并且HTMLCanvasElement类型元素上是存在getContext方法的,也就能够合法的调用getContext

你可能会想,那我直接把myCanvas声明为HTMLCanvasElement类型不就行了?比如下列代码:

const myCanvas: HTMLCanvasElement = document.getElementById("main_canvas")

然而很遗憾,这是行不通的。这里我们直接将其转换为一个更简单的形式帮助理解:

const getString = ():string => {
    return 'tomcat'
}

const num: number = getString();
// Type 'string' is not assignable to type 'number'.ts(2322)

上面的代码试图将一个string结果赋值给一个number类型的变量,这显然是无法通过ts检查的,而这就和上面的代码是一样的道理。也就是我们无法通过类型声明来告诉tsmyCanvasHTMLCanvasElement类型。

那要怎么做呢?答案是断言。

const myCanvas = document.getElementById("canvas") as HTMLCanvasElement
const context = myCanvas.getContext('2d')

// or
const myCanvas = document.getElementById("canvas")
const context = (myCanvas as HTMLCanvasElement).getContext('2d')

上面的代码明确告诉tsdocument.getElementById会返回一个HTMLCanvasElement元素,而HTMLCanvasElement上是存在getContext方法的,所以后面的getContext也就属于合法调用,也就不会报错了。

断言的问题

断言相当于移除了编译时的类型检查,也就无法在编译阶段检查出异常情况,所以如果你无法保证断言始终为真,就会导致运行时的异常。

const myCanvas = document.getElementById("canvas") as HTMLCanvasElement
const context = myCanvas.getContext('2d');

比如上面的断言代码,就要求document.getElementById("canvas")始终返回一个有效的HTMLCanvasElement的类型元素。而我们知道document.getElementById返回null的可能性是无法排除的。
当页面上没有canvas元素,null就会被断言为HTMLCanvasElement,其结果就会导致异常。所以这里就要求我们用运行时的代码进行检测。

加上运行时检测:

const myCanvas = document.getElementById("canvas") as HTMLCanvasElement
if (myCanvas) {
    const context = myCanvas.getContext('2d')
}

这样,就算null被断言为HTMLCanvasElement类型,在运行时,它仍然还是个null,也就无法绕过if语句进而导致异常了。

更好的方式

我们知道了断言的弊端,那有没有更好的方式呢?

答案是用类型守卫来取代避免断言。

const myCanvas = document.getElementById("canvas")
if (myCanvas instanceof HTMLCanvasElement) {
  const context = myCanvas.getContext('2d')
}

这里其实做了两件事:
1.剔除了myCanvasnull的情况,因为null的原型链上是不会有HTMLCanvasElement的。
2.判断myCanvas是否为HTMLCanvasElement类型。

而且这里的代码意图更明显,也更容易理解,所以更推荐使用类型守卫而不是断言。

资料

type-assertions


热饭班长
3.7k 声望434 粉丝

先去做,做出一坨狗屎,再改进。