Typeforhold via 'extends' i TypeScript?
Så vidt jeg forstår innebærer A extends B at A er "minst innenfor" B. Så for unioner betyr A extends B at minst ett tilfelle av B er tilfredsstilt av A, og A har ingen tilfeller utenfor B, og for produkter betyr det at A har minst egenskapene til B.
Hvis det er korrekt, forstår jeg ikke følgende:
function doesExtend<A, B>(_: (A extends B ? 'yes' : 'no')) { }
doesExtend<'a', 'a'>('yes'); // correct: yes, identical
doesExtend<'a' | 'b', 'a' | 'b'>('yes'); // correct: yes, still identical
doesExtend<'b' | 'a', 'a' | 'b'>('yes'); // correct: yes, still identical
doesExtend<'a' | 'b', 'a' | 'b'>('no'); // correct: no, because they are idential right?
doesExtend<'a', 'a' | 'b'>('yes'); // correct: yes, subset is within a superset
doesExtend<'a' | 'b', 'a'>('no'); // correct: no, superset is larger than a subset
doesExtend<'a' | 'b', 'a'>('yes'); // <--- HERE! wtf? (incorrect yes)
Nå, for å gjøre det enda verre:
type WhoExtendsWho<A, B> = A extends B
? B extends A
? 'A extends B, and B extends A'
: 'A extends B, but B does not extend A'
: B extends A
? 'A does not extend B, but B extends A'
: 'A does not extend B, and B does not extend A';
type X = WhoExtendsWho<'a' | 'b', 'a'>; // "A extends B, and B extends A" | "A does not extend B, and B does not extend A"
Så mitt spørsmål er: hvordan kan binær dikotomi gi meg disse to svarene?
Svar #1
Typescript extends er distributivt over unions - det vil si, gitt
type ExtendsQ<A, B> = A extends B ? true : false;
Alle følgende typer er ekvivalente:
type Original = ExtendsQ<'a' | 'b', 'b'>;
type Aliased = ExtendsQ<'a', 'b'> | ExtendsQ<'b', 'b'>;
type Expanded = ('a' extends 'b' ? true : false) | ('b' extends 'b' ? true : false);
type Simplified = false | true;
type FinalResult = boolean;
(denne distribusjonen skjer i «extends», ikke tidlig i selve typen, men jeg sliter med å angi det på en klar måte)
Jeg tror dette er en veldig forvirrende og upraktisk funksjon, men heldigvis finnes det en enkel utvei: spør det samme om 1-tupler av A og B. Og sannsynligvis er det en god ting, da det hadde vært vanskeligere, om ikke umulig, å gå i en annen retning hvis de skulle standardisere til ikke-distribuerende oppførsel. Ved å bruke den samme definisjonen som ovenfor, la oss utforske hvordan dette fungerer:
type ExtendsQ<A, B> = A extends B ? true : false;
type Res1 = ExtendsQ<'a' | 'b', 'a'>; // boolean
// ^?
type Res2 = ExtendsQ<['a' | 'b'], ['a']>; // false
// ^?
type Res3 = ExtendsQ<['a'], ['a' | 'b']>; // true
// ^?
type Res4 = ExtendsQ<['a'], ['a']>; // true
// ^?
Fint, ikke sant? Det ser ut til å samsvare med intensjonen din. Det er fordi en komponent på toppnivå i extends ikke lenger er en union, så det er ingenting å distribuere.
Nå kan du gjøre det samme på alias-siden for å gjøre det til standardadferd:
function doesExtend<A, B>(_: ([A] extends [B] ? 'yes' : 'no')) { }
doesExtend<'a', 'a'>('yes'); // correct: yes, identical
doesExtend<'a', 'a' | 'b'>('yes'); // correct: yes, subset extends a superset
doesExtend<'a' | 'b', 'a'>('no'); // correct: no, superset doesn't extend a subset
doesExtend<'a' | 'b', 'a'>('yes'); // error, hooray
type WhoExtendsWho<A, B> = [A] extends [B]
? [B] extends [A]
? 'A <: B, and B <: A'
: 'A <: B, but not B <: A'
: [B] extends [A]
? 'not A <: B, but B <: A'
: 'not A <: B, and not B <: A';
type X = WhoExtendsWho<'a' | 'b', 'a'>; // "not A <: B, but B <: A"
Og her er en lekeplass med all koden fra svaret mitt for å prøve det interaktivt.
