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.

Relaterte artikler