Typescript: kan ikke hente verditypen til objektet med merket typevakt


Jeg vet ikke hvorfor objektverditypen ikke kan hentes ut riktig med generisk som nedenfor. Kan du fortelle meg hvorfor det ikke fungerer hvis du vet det?

export type ValueType = {
    type: "string",
    value: string,
} | {
    type: "number",
    value: number,
}

/** 1. Extract value type of "string" type */
type StringValueType = (ValueType & {type: "string"})["value"];
// type StringValueType = string

/** 2. Extract value type of each type by generic */
type ValueTypeWithGeneric<T extends ValueType["type"]> = (ValueType & {type: T})["value"];
type StringValueTypeWithGeneric = ValueTypeWithGeneric<"string">;
// type StringValueTypeWithGeneric = string | number
// I expect this type to be string

Svar #1

Dette er en designbegrensning i TypeScript; se microsoft/TypeScript#45428 for et autoritativt svar. Når man står overfor en type av følgende form

type X<T> = ({ a: 0, b: "x" } & { a: T })["b"] 

kompilatoren antar at dette alltid vil være ekvivalent med

type X<T> = ({ a: 0 & T, b: "x" })["b"]

uansett hva den generiske typeparameteren T vil være. Og derfor reduserer kompilatoren dette raskt til

type X<T> = "x"

Så X<T> er bare "x" uavhengig av T:

type XT0 = X<0> // "x" 👍
type XT1 = X<1> // "x" 👎

Men når du faktisk gjør dette med en ikke-generisk type i stedet for T, ser du en annen oppførsel:

type X0 = ({ a: 0, b: "x" } & { a: 0 })["b"] // "x"
type X1 = ({ a: 0, b: "x" } & { a: 1 })["b"] // never 

Det er diskrepansen mellom X<1> og X1 som du har støtt på.


I TypeScript 3.8 og lavere fantes det ikke noen avvik. Typen X1 ville også evaluere til "x":

// TS3.8 and below
type X1 = ({ a: 0, b: "x" } & { a: 1 })["b"] // "x" in TS3.8-

Selv om det ikke er mulig å ha en verdi med en a-egenskap av både type "0" og type "1", beholdt kompilatoren {a: 0, b: "x"} & {a: 1} som den er, og derfor blir b-egenskapen sett på som av typen "x".

Men TypeScript 3.9 introduserte støtte for å redusere snitt ved hjelp av diskriminerende egenskaper, slik det er implementert i microsoft/TypeScript#. Nå ser kompilatoren {a: 0, b: "x"} & {a: 1} som typen never, og dermed er også b-egenskapen av typen never.

Og det er flott. Men dessverre ble ikke oppførselen overfor generiske typer oppdatert for å ta høyde for denne endringen. Når man krysser med generisk {a: T}, skjer ingen reduksjon, uansett hva. Å ja vel. Det er en designbegrensning.


Den anbefalte løsningen her er å unngå å bruke kryssninger (intersections) for dette formålet, og i stedet bruke nytte-typen Extract<T, U>, som har til hensikt å filtrere en unionstype T til kun de medlemmene som kan tildeles U. Så i stedet for {a: 0, b: "x"} & {a: T}, kan du skrive Extract<{a: 0, b: "x"}, {a: T}> og få konsistent atferd:

type Ex<T> = Extract<{ a: 0, b: "x" }, { a: T }>["b"] 
type Ex0 = Ex<0> // "x" 👍
type Ex1 = Ex<1> // never 👍

Og det betyr at eksemplet ditt også vil begynne å oppføre seg som ønsket:

/** 1. Extract value type of "string" type */
type StringValueType = Extract<ValueType, { type: "string" }>["value"];
// type StringValueType = string 👍

/** 2. Extract value type of each type by generic */
type ValueTypeWithGeneric<T extends ValueType["type"]> = Extract<ValueType, { type: T }>["value"];
type StringValueTypeWithGeneric = ValueTypeWithGeneric<"string">;
// type StringValueTypeWithGeneric = string 👍

Playground-lenke til kode i TS4.6

Playground-lenke til kode i TS3.8

Relaterte artikler