When working with TypeScript, you can pass a value to a function and have that function assert the value is true for type narrowing. For example:
function assertTrue(v: unknown, message: string): asserts v {
if (!v) {
throw new MySpecialError(message)
}
}
But what if you want a higher-level function that constructs a v
? For instance:
function assertEqual(v: unknown, expected: unknown) {
assertTrue(v === expected, `expected ${v} to be ${expected}`)
}
Unfortunately, calling assertEqual
does not result in any type narrowing. A workaround is:
function assertEqual<T>(v: unknown, expected: T): asserts v is T {
assertTrue(v === expected, `expected ${v} to be ${expected}`)
}
However, this would require calls like
assertEqual<'foo'>(v, 'foo')
, which is prone to mistakes.
In the following example, nameOf
and nameOf3
both pass type checking, but nameOf2
does not:
export type Staged =
| {
stage: 'pre-named';
}
| {
stage: 'named';
name: string;
};
class MySpecialError extends Error {}
function assertTrue(v: unknown, message: string): asserts v {
if (!v) {
throw new MySpecialError(message)
}
}
function nameOf(staged: Staged) {
assertTrue(staged.stage === 'named', 'must be named')
return staged.name
}
function assertEqual<T>(v: unknown, expected: T): asserts v is T {
assertTrue(v === expected, `expected ${v} to be ${expected}`)
}
function nameOf2(staged: Staged) {
assertEqual(staged.stage, 'named')
return staged.name
}
function nameOf3(staged: Staged) {
assertEqual<'named'>(staged.stage, 'named')
return staged.name
}