typescript - att returnera T inuti en typ resulterar i never när ett strängenum skickas som generisk typ


Betrakta följande enum A:

enum A {
  ONE,
  TWO = "TWO"
}

Jag vill skriva en generell type som kontrollerar om en string eller number är en del av A. Detta är vad jag har kommit fram till:

type IsAValue<T extends string | number> = T extends string
  ? T extends `${A}`  
    ? "true1"
    : "false1"
  : `${T}` extends `${A}` 
    ? "true2"
    : "false2"

Det verkar fungera bra.

type Test1 = IsAValue<0>
//   Test1 = "true2"

type Test2 = IsAValue<A.ONE>
//   Test2 = "true2"

type Test3 = IsAValue<"TWO">
//   Test3 = "true1"

type Test4 = IsAValue<A.TWO>
//   Test4 = "true1"

type Test5 = IsAValue<9999>
//   Test5 = "false2"

type Test6 = IsAValue<"NOT IN ENUM">
//   Test6 = "false1"

Men nu vill jag inte returnera "true1" eller "true2" utan istället bara returnera T.

type IsAValue2<T extends string | number> = T extends string
  ? T extends `${A}`  
    ? T
    : "false1"
  : `${T}` extends `${A}` 
    ? T
    : "false2"

Ändå bryts detta oväntat för A.TWO.

type Test7 = IsAValue2<A.TWO>
//   Test7 = never

Detta är märkligt eftersom vi precis såg att det utvärderas till "true1" i IsAValue. När jag returnerar T skulle jag helt enkelt förvänta mig att A.TWO är resultatet här.

Varför blir detta never om never inte ens är en typ i någon gren av IsAValue2? Och varför fungerar detta fortfarande för de andra typerna?

Lekplats

Svar #1

Detta är en känd bugg i TypeScript, se microsoft/TypeScript#41778. Även om A.TWO extends "TWO" anses vara sant, manipulerar inte kompilatorn enum-typer som man skulle förvänta sig. Specifikt, A.TWO & "TWO" utvärderas till never, även om det (enligt min åsikt) borde utvärderas till A.TWO. Se microsoft/TypeScript#21998 för ett problem om snittet. Och i den sanna grenen av konditionen ändrar kompilatorn T till T & "TWO" för att kunna känna igen att T faktiskt kan tilldelas "TWO". Och det är never. Det är lite rörigt.

Kanske kommer den buggen att fixas vid någon tidpunkt, men vem vet när det blir. Under tiden finns det lösningar. En skulle vara att "kopiera" typen innan man kontrollerar den. I fallet med en strängenum, skulle det enklaste här vara att också omvandla den via en mallsträng innan man kontrollerar den:

type IsAValue2<T extends string | number> = T extends string
  ? `${T}` extends `${A}` // <-- this change here
  ? T
  : "false1"
  : `${T}` extends `${A}`
  ? T
  : "false2"

type Test7 = IsAValue2<A.TWO>
//   Test7 =  A.TWO             

Detta utvärderas som önskat för Test7 och de andra ändras inte. Det antyder att du bör sammanfoga hela saken till

type IsAValue<T extends string | number> =
  `${T}` extends `${A}` ? T : "false"

I det allmänna fallet kan du kopiera en parameter genom att använda villkorlig typeninferens, som T extends infer U ? ... : never:

type IsAValue<T extends string | number> = T extends string
  ? (T extends infer U ? U extends `${A}` ? T : "false1" : never)
  : `${T}` extends `${A}` ? T : "false2"

Detta ger också de önskade resultaten.

Lekplatslänk till kod

Relaterade artiklar