typescript / expert
Snippet
Typgleichheits-Assertions zur Compile-Zeit für Typ-Tests
Library-Autoren testen Laufzeitverhalten mit Vitest oder Jest, aber Typ-Verträge brauchen ein eigenes Gate. Der `Equal<A, B>`-Trick nutzt aus, dass zwei Funktionstypen mit internen Conditionals nur dann gegenseitig zuweisbar sind, wenn diese Conditionals für jedes `T` identisch sind — was strukturelle und Varianz-Gleichheit erzwingt, anders als das naive `A extends B ? B extends A ? true : false : false`, das bivariant Subtypen durchwinkt. `Expect<T extends true>` macht das zur Waffe: der Typ resolved nur, wenn `T` `true` ist — eine fehlschlagende Assertion leuchtet im Editor rot auf. Kombiniert mit `tsc --noEmit` in der CI ist die ausgelieferte `.d.ts` festgenagelt.
snippet.ts
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Equal<A, B> =(<T>() => T extends A ? 1 : 2) extends(<T>() => T extends B ? 1 : 2)? true: false;type Expect<T extends true> = T;// Test cases — these are compile-time assertions, not runtime checks:type _01 = Expect<Equal<Awaited<Promise<Promise<string>>>, string>>;type _02 = Expect<Equal<Capitalize<'foo'>, 'Foo'>>;type _03 = Expect<Equal<NonNullable<string | null | undefined>, string>>;// Naive equality FAILS to distinguish these — `Equal` succeeds:type Naive<A, B> = A extends B ? (B extends A ? true : false) : false;type N1 = Naive<{ a: 1 }, { a: 1; b?: 2 }>; // true (wrong!)type E1 = Equal<{ a: 1 }, { a: 1; b?: 2 }>; // false (correct)// Would fail to compile — intentional:// type _bad = Expect<Equal<string, 'foo'>>;
Erklärung
1
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2)
Verpackt die Gleichheitsprüfung in generische Funktionstypen — Zuweisbarkeit gilt nur, wenn das innere Conditional für jedes `T` identisch reagiert.
2
type Expect<T extends true> = T;
Die Constraint `T extends true` macht den Alias unerfüllbar, wenn die Gleichheit `false` liefert — daraus entsteht der Compile-Fehler.
3
type _01 = Expect<Equal<Awaited<Promise<Promise<string>>>, string>>;
Behauptet, dass `Awaited` verschachtelte Promises rekursiv auspackt — ein Vertrag, den man sonst gegen die Doku gegenlesen müsste.
4
type N1 = Naive<{ a: 1 }, { a: 1; b?: 2 }>;
Zeigt, warum bidirektionales `extends` nicht reicht: eine optionale Property ist bivariant in beide Richtungen zuweisbar — der naive Check besteht fälschlich.