typescript - varför ConditionalType löses till en annan typ än förväntat?


Jag undrar varför TypeScript löser villkorlig typ till never om jag lägger till en begränsning som borde garantera önskad typ.

Exempel

Låt oss säga att vi har skrivit sådan kod

type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null;

type ConditionalType<T> = T extends PrimitiveDataType
  ? (v: T) => void
  : never;

abstract class AbstractClass<T> {
  abstract value: T;
  protected conditionalFunctions: Map<ConditionalType<T>, number | undefined> = new Map();
}

class SomeClass<T extends PrimitiveDataType> extends AbstractClass<T> {
  value: T;

  constructor(value: T) {
    super();
    this.value = value;
  }

  someMethod() {
    for (const someFn of this.conditionalFunctions.keys()) {
      someFn(this.value);
    }
  }
}

TS Playgrounds länk :

I koden ovan har jag skapat en PrimitiveDataType som är en union av alla primitiva datatyper i JavaScript.

Sedan har jag skapat en ConditionalType<T> som kommer att lösa till någon callback endast om T är en av PrimitiveDataType.

Då har jag skapat en abstrakt generisk klass som har ett fält vars typ (ConditionalType<T>) beror på det generiska värdet av denna klass.

I slutändan har jag skapat en SomeClass som utökar AbstractClass<T> och lagt till begränsningar att den generiska parametern T måste utöka PrimitiveDataType och jag får detta fel:

TS2345: Argument of type 'PrimitiveDataType' is not assignable to parameter of type 'never'.   
Type 'undefined' is not assignable to type 'never'.

Slutsats

Jag trodde att om i SomeClass T har en begränsning att den måste vara en på PrimitiveDataType så skulle TypeScript lösa fältet conditionalFunctions till att vara av typen Map<(v: T) => void, number | undefined>. Till min förvåning löser TypeScript denna typ till Map<(v: never) => void, number | undefined>, vilket inte är tydligt för mig och jag vet inte var felet ligger i sättet jag tänker på detta.

Kan du förklara för mig varför det fungerar så här, eller kanske är det en bugg i TypeScript-kompilatorn?

Observationer

Om jag lämnar bara en, skriv in PrimitiveDataType så fungerar allt okej, men för mer än en får jag ett fel

Redigera 1

Om min villkorade typ har fler än två möjliga returtyper, fungerar inte distributiv [T] och ger ett sådant fel:

Expected 2 arguments, but got 1.

Det ser ut som om TS nu löser denna typ till typ som är mappad för Array. Jag förstår det helt enkelt inte nu

Exempel

type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null;

type ConditionalType<T> = [T] extends [PrimitiveDataType]
  ? (v: T) => void
  : T extends Array<unknown>
  ? (v: T, t: number) => void
  : never;

abstract class AbstractClass<T> {
  abstract value: T;
  protected conditionalFunctions: Map<ConditionalType<T>, number | undefined> = new Map();
}

class SomeClass<T extends PrimitiveDataType> extends AbstractClass<T> {
  value: T;

  constructor(value: T) {
    super();
    this.value = value;
  }

  someMethod() {
    for (const someFn of this.conditionalFunctions.keys()) {
      someFn(this.value);
    }
  }
}

TS-lekplats - REDIGERA 1 :

Enligt min åsikt bör detta fungera på samma sätt som i det här exemplet:

type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null;

type ConditionalType<T> = [T] extends [PrimitiveDataType]
  ? (v: T) => void
  : T extends Array<unknown>
  ? (v: T, t: number) => void
  : never;

const x: PrimitiveDataType = 12;
const y: ConditionalType<typeof x> = (param: number) => undefined;

TS Playground - exempel på variabler :

Det finns inget problem och ConditionalType<typeof x> löses till (v: number) => void vilket är vad jag skulle förvänta mig.

Svar #1

Din generiska typ ConditionalType<T> producerar en distributiv typ när den ges en union.

Till exempel när man får unionen string | number resulterar det i följande typ

type Foo = ConditionalType<string | number>
// ((v: string) => void) | ((v: number) => void)

Omkapsla vilken sida som helst av villkoret i hakparenteser för att undvika detta

type ConditionalType<T> = [T] extends [PrimitiveDataType]
  ? (v: T) => void
  : never;

Ovan genererar nu följande, icke-distributiva, typ som borde fungera för din abstrakta klass

type Foo = ConditionalType<string | number>
// (v: string | number) => void

Lekplats

Svar #2

Jag är ganska junior på TypeScript, jag har använt det i mer eller mindre ett och ett halvt år, men jag kommer ändå försöka hjälpa dig och ge dig en möjlig förklaring till felet du stötte på (åtminstone vad jag tror är förklaringen), och möjliga sätt att åtgärda det.

/* =============== FÖRKLARING =============== */

En anledning, och den som verkar mest rimlig för mig här, kan vara att typavsmalningen inte var tillräcklig. Varför?

I koden du tillhandahöll definierade du typen ConditionalType<T> som en funktion som returnerar void när T är en PrimitiveDataType och never när T inte är en PrimitiveDataType. Detta är logiskt korrekt, men sättet du använder denna funktionstyp kan vara inte det bästa. Syftet med denna funktionstyp är att begränsa typen av T och bör användas som ett typvakt. När du deklarerar klassen SomeClass är dock typen T en utvidgning av den råa PrimitiveDataType och inte den begränsade typen, så i for-loopen kontrollerar du om en rå typ PrimitiveDataType är en giltig key för den begränsade typversionen (som du deklarerade i AbstractClass), men naturligtvis existerar inte never på PrimitiveDataType. Det är därför du bör utöka T i klassdeklarationen SomeClass som visas i den andra lösningen jag gav dig nedan. På så sätt kommer T också att inkludera never som standard. Och så kommer value-typen att vara den förträngda versionstypen, alltså ConditionalType<PrimitiveDataType>, och inte PrimitiveDataType.

Jag hoppas att du förstod min poäng (min engelska är fortfarande inte så bra haha).

/* =============== FIXAR =============== */

Jag kommer att ge dig två sätt att fixa detta, där det andra är det mest typsäkra.

Jag tror att du kan fixa detta på två sätt:

Det första skulle vara att definiera PrimitiveDataType för att alltid vara en funktionstyp:

type ConditionalType<T> = (v: T) => void;

Självklart, i fallet ovan kanske ConditionalType inte är det rätta namnet längre, hitta ett bättre. Länk till Typescript-lekplatsen för fallet som beskrivs ovan -> Typescript-lekplats

/* =============== ELLER =============== */

Eller så kan du använda detta andra tillvägagångssätt där typparametern T är begränsad till att vara en subtype av typen ConditionalType<PrimitiveDataType>. I det här fallet utökar T ConditionalType<PrimitiveDataType>, vilket betyder att T måste vara en funktionstyp som tar ett argument av typen PrimitiveDataType och returnerar void.

type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null;

type ConditionalType<T> = T extends PrimitiveDataType
  ? (v: T) => void
  : never;

abstract class AbstractClass<T> {
  abstract value: T;
  protected conditionalFunctions: Map<ConditionalType<T>, number | undefined> = new Map();
}

class SomeClass<T extends ConditionalType<PrimitiveDataType>> extends AbstractClass<T> {
  value: T;

  constructor(value: T) {
    super();
    this.value = value;
  }

  someMethod() {
    for (const someFn of this.conditionalFunctions.keys()) {
        someFn(this.value);        
    }
  }
}

Länk till TypeScript-lekplatsen för det andra tillvägagångssättet -> TypeScript-lekplats

/* =============== REDIGERA 1 =============== */

Vad sägs om REDIGERING 1

Förklaringen till felet du stöter på när du försöker använda mer än en parameter är nästan densamma, det är alltid relaterat till typbegränsning, men lösningen är väldigt, väldigt enkel. Du glömde bara att göra den andra parametern valfri.

/* =============== FIX EDIT 1 =============== */

type ConditionalType<T> = T extends PrimitiveDataType
  ? (v: T) => void
  : T extends Array<unknown>
  ? (v: T, t?: number) => void
  : never;

Här finns TypeScript-playgrounden med hela koden för fixen av EDIT 1 -> TypeScript-playground

Vänligen låt mig veta om detta var till hjälp. :)

Relaterade artiklar