typescript - Finns det ett sätt att skapa en mappad typ som endast tar valfria eller arrayvärden


Jag har typen: type Primitive=number | string | boolean | object | symbol; och en funktion som jag vill vara säker på får objekt som har alla egenskaper av typen Primitive | undefined eller Primitive[] men som inte kan ha egenskaper av typen Primitive. Jag försökte skapa type OptionalPrimitive=(number | undefined) | (string | undefined) | (boolean | undefined) | (object | undefined) | (symbol | undefined); och senare använda den i en mappad typ:

type SelectorProps = {
    [key: string]: OptionalPrimitive | Primitive[];
    // or [key: string]: (Primitive | undefined) | Primitive[];
}

men det fungerar inte som förväntat.

Så jag vill skapa en typ som är kompatibel med objekt av denna typ

type T1 = {
    a?: boolean
    b?: Object
    c: string[]
}

Men inte av den här typen:

type T2 = {
    a: boolean,
    b?: string[],
}

Svar #1

Det finns ingen specifik typ i TypeScript som fungerar på detta sätt. Eftersom {a: boolean} kan tilldelas till {a?: boolean}, så om {a?: boolean} är avsedd att kunna tilldelas till SelectorProps, då kommer även {a: boolean} att kunna det.

Istället skulle du behöva skriva en generisk typ, SelectorProps<T>, som fungerar som en begränsning på T. Det är mer som en validering än en typ. Om T extends SelectorProps<T>, då är det giltigt; annars är det ogiltigt.

Här är ett möjligt tillvägagångssätt:

type SelectorProps<T extends object> =
  { [K in keyof T]: {} extends Pick<T, K> ? Primitive : Primitive[] }

Den betingade typkontrollen, {} extends Pick<T, K> kontrollerar om K är valfri i T eller inte. Insikten är att {} extends {a?: any} men inte {a: any}. Och Pick<T, K> tittar bara på den del av T som har egenskapen K. Så om {} extends Pick<T, K> så är K valfri, annars inte.


Den typen fungerar, men på egen hand är den verkligen irriterande att använda direkt eftersom du skulle behöva skriva ut din typ två gånger, som

type T1 = {
  a?: boolean
  b?: object
  c: string[]
}
declare const t1: T1;
const sp1: SelectorProps<T1> = t1; // okay

type T2 = {
  a: boolean,
  b?: string[],
}
declare const t2: T2;
const sp2: SelectorProps<T2> = t2; // error!

Det vore trevligt om du bara kunde skriva SelectorProps eller SelectorProps<infer> och låta TS härleda typargumentet istället för att skriva typer två gånger, men detta är en saknad funktion i TypeScript som efterfrågats på microsoft/TypeScript#32794. Tills dess och om det inte implementeras behöver vi ett annat angreppssätt.


TypeScript kommer inte att härleda generiska typargument i generiska typer, men det kommer att göra det när du anropar generiska funktioner. Så vi kan skriva en generisk hjälpfunktion som bara returnerar sitt input och används för att härleda typargumentet:

const selectorProps = <T extends SelectorProps<T>>(t: T) => t;

declare const t1: T1;
const sp1 = selectorProps(t1); // okay

declare const t2: T2;
const sp2 = selectorProps(t2); // error!
/* Argument of type 'T2' is not assignable to parameter of type 'SelectorProps<T2>'.
  Types of property 'a' are incompatible.
    Type 'boolean' is not assignable to type 'Primitive[]'.(2345) */

Nu fungerar det som önskat, där du får felmeddelanden som berättar vad som är fel med din typ om den inte matchar din avsedda begränsning. Om du kisar kan du se const sp1=selectorProps(t1) som liknande const sp1: SelectorProps=t1, förutom att funktionsanropsversionen faktiskt fungerar.

Lekplatslänk till kod