TypeScript is powerful, but was retrofitted upon JavaScript and the DOM API. Some APIs work pooly with TypeScript as the type information is missing. A common issue is document.getElementById
can return any HTML element.
The problem
document.body.insertAdjacentHTML('beforeend', '<canvas id="main-content">');
const canvas = document.getElementById('main-content');
// TypeScript can only assume canvas is a HTMLElement
const ctx = canvas.getContext('2d');
// TypeScript error
// Object is possibly 'null'.
// Property 'getContext' does not exist on type 'HTMLElement'.
Playground
Type assertion
The TypeScript as
operator is a Type assertion. It is a form of Type conversion and tells TypeScript, "I know what I'm doing, this thing is really that type".
document.body.insertAdjacentHTML('beforeend', '<canvas id="main-content">');
const canvas = document.getElementById('main-content') as HTMLCanvasElement;
// TypeScript has been told canvas is HTMLCanvasElement
const ctx = canvas.getContext('2d');
Playground
Lying type assertions
However, note type assertions tell TypeScript, "I know what I'm doing". What if that is no longer true? What if we decide main-content
is no longer a <canvas>
but now an <div>
? The code would fail, but it would fail at the time in which getContext
is called. What if getContext
is in another module?
document.body.insertAdjacentHTML('beforeend', '<div id="main-content">');
const canvas = document.getElementById('main-content') as HTMLCanvasElement;
// TypeScript has been lied to that canvas is HTMLCanvasElement
const ctx = canvas.getContext('2d');
// Runtime error
// canvas.getContext is not a function
Playground
That error message is confusing. It would be nice if the error was closer to the source of the issue.
Assertion functions to the rescue
The issue is type assertions exist only at compile time to tell TypeScript information. There is no running JavaScript code that actually checks the type at runtime. This is where Assertion functions come in. Assertion functions assert types using actual running code.
function assertInstanceOf<T>(value: any, expectedClass: new () => T): asserts value is T {
if (!(value instanceof expectedClass)) {
throw new Error(`Expected value to be a ${expectedClass.name}, but was ${value.constructor.name}`);
}
}
document.body.insertAdjacentHTML('beforeend', '<canvas id="main-content">');
const canvas = document.getElementById('main-content');
assertInstanceOf(canvas, HTMLCanvasElement);
// TypeScript now knows canvas is a HTMLCanvasElement
const ctx = canvas.getContext('2d');
Playground
The strange type new () => T
simply means "a class of type T". The magic part is asserts value is T
. This is a promise to TypeScript that, "This function will assert value is type T and will throw an error otherwise"
Now when main-content
becomes a <div>
, an error is thrown.
function assertInstanceOf<T>(value: any, expectedClass: new () => T): asserts value is T {
if (!(value instanceof expectedClass)) {
throw new Error(`Expected value to be a ${expectedClass.name}, but was ${value.constructor.name}`);
}
}
document.body.insertAdjacentHTML('beforeend', '<div id="main-content">');
const canvas = document.getElementById('main-content');
assertInstanceOf(canvas, HTMLCanvasElement);
// Runtime error
// Expected value to be a HTMLCanvasElement, but was HTMLDivElement
const ctx = canvas.getContext('2d');
Playground
Excellent! The error is now close to where canvas is first declared making it far easier to fix.
Do you want to write honest TypeScript? You're in luck, Battlefy is hiring.