typescript - Kartläggning av en typ med P i K bevarar inte modifikatorer


Jag har följande kod:

export type Message =
  { type: 'auth', user: string, password?: string } |
  { type: 'getData'}

export type Answer = 
  { type: 'auth', success: boolean } |
  { type: 'getData', someData: any}

export type MessageType = Message["type"]
export type ExcludeTypeField<A> = { [K in Exclude<keyof A, "type">>]: A[K] }
export type ExtractParameters<A, T> = ExcludeTypeField<Extract<A, { type: T }>>

type a = ExtractParameters<Message, 'auth'>

Det tillåter mig att extrahera typinformation för Message- och Answer-typer genom att endast ange type-fältet. Av någon anledning tas dock den valfria modifieraren för password bort. Jag antar att det beror på att typmappningen inte bryr sig om vad som en gång var valfritt och bara skapar en ny typ med alla nycklar.

Jag arbetade runt detta problem genom att ersätta ExcludeTypeField med:

export type OptionalKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? K : never }[keyof T]
export type ExcludeTypeField<A> = { [K in Exclude<Exclude<keyof A, "type">, OptionalKeys<A>>]: A[K] } & { [K in Extract<Exclude<keyof A, "type">, OptionalKeys<A>>]?: A[K] }

Detta är väldigt ordrikt eftersom jag i princip bara slår ihop två typmappningar (en gång exkluderar jag alla valfria parametrar och en gång väljer jag dem inte medan jag lägger till den valfria modifieraren tillbaka). Jag känner att det borde finnas ett enklare sätt. Finns det?

TypeScript-lekplats

Svar #1

Du letar förmodligen efter hjälpverktygstypen Omit<T, K>, som tar en typ T och tar bort alla egenskaper med nycklar i K från typdefinitionen, samtidigt som eventuella modifierare i T bevaras:

type ExtractParameters<A, T> = Omit<Extract<A, { type: T }>, "type">

Du kan verifiera att det fungerar som önskat:

type A = ExtractParameters<Message, 'auth'>
/* type A = {
    user: string;
    password?: string | undefined;
} */

Förresten, Omit<T, K> är inte någon inbyggd TypeScript-magik; det är en del av standardbiblioteket och du hade kunnat definiera det själv. Din ExcludeTypeField är nära, men saknar ett avgörande lager av indirektion för att göra mappningen homomorfisk (vilket, bland annat, bevarar modifierare). Du behöver antingen att den mappade typen inkluderas K in keyof XYZ direkt, eller, K in P där P är en generisk typparameter deklarerad som P extends keyof XYZ. Se microsoft/TypeScript#12826 för PR:en som implementerar detta.

Så du skulle behöva ändra detta:

type ExcludeTypeField<A> = { [K in Exclude<keyof A, "type">>]: A[K] }

till något liknande detta:

type _ExcludeTypeField<A, P extends keyof A> = { [K in P]: A[K] }
type ExcludeTypeField<A> = _ExcludeTypeField<A, Exclude<keyof A, "type">>

och då skulle det ha fungerat för dig på det sätt du ville:

type B = ExcludeTypeField<{ type: string; a?: number; readonly b: boolean }>
/* type B = {
    a?: number | undefined;
    readonly b: boolean;
} */

Men självklart är det mycket enklare att bara använda Omit<T, K> som redan hanterar det krångliga åt dig.

Lekplatslänk till kod

Svar #2

Verktygstypen Extract är en utmärkt matchning för detta. Extract låter dig få alla medlems typer i en union som kan tilldelas till ditt givna indata.

Till exempel, givet dina ovanstående typer,

// Foo is { type: 'auth'; user: string; password?: string | undefined; }
type Foo = Extract<Message, { type: 'auth' }>;

Den här typen kan sedan göras generisk som du redan har visat ovan.