In typescript you can pass a value to a function and have that function assert the value is true for the sake of type narrowing. eg
function assertTrue(v: unknown, message: string): asserts v {
if (!v) {
throw new MySpecialError(message)
}
}
However, what if you wanted to have a higher level function that constructed a v
. eg
function assertEqual(v: unknown, expected: unknown) {
assertTrue(v === expected, `expected ${v} to be ${expected}`)
}
If you call assertEqual
you do not get any type narrowing. A hack solution is
function assertEqual<T>(v: unknown, expected: T): asserts v is T {
assertTrue(v === expected, `expected ${v} to be ${expected}`)
}
but that requires calls like assertEqual<'foo'>(v, 'foo')
which is easy to get wrong.
In this example, nameOf
and nameOf3
both type check 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
}
CodePudding user response:
You can derive the asserted generic type information from the second parameter like this:
function assertStrictEquals <T>(actual: unknown, expected: T): asserts actual is T {
if (actual !== expected) throw new Error('Values not equal');
}
const expected = { msg: 'hello world' };
const actual: unknown = expected; // unknown
assertStrictEquals(actual, expected);
actual; // { msg: string; }
And, as pointed out by futur, you can use a const
assertion to instruct the compiler to infer a string literal type from an argument:
function nameOf2 (staged: Staged): string {
assertStrictEquals(staged.stage, 'named' as const);
return staged.name;
}
You can create an assertion function exclusively for inferring string literal values:
function assertIsString <T extends string>(actual: string, expected: T): asserts actual is T {
assert(actual === expected, 'Strings not equal');
}
CodePudding user response:
Try using Template Literal Types:
type AssertType<T> = `${any & T}`;
function assertEqual<K, T extends (unknown extends T ? AssertType<K> : K)>(v: unknown, expected: T): asserts v is T {
assertTrue(v === expected, `expected ${v} to be ${expected}`)
}
function nameOf(staged: Staged) {
assertTrue(staged.stage === 'named', 'must be named')
return staged.name
}
function nameOf2(staged: Staged) {
assertEqual(staged.stage, 'named')
return staged.name
}
function nameOf3(staged: Staged) {
assertEqual<string, 'named'>(staged.stage, 'named')
return staged.name
}
Full example at TS Playground.