ข้ามไปยังเนื้อหา

Conditional Types

conditional type เลือกหนึ่งในสอง type ตามความสัมพันธ์ ใช้ syntax ternary ที่คุณรู้จักอยู่แล้ว — แต่ในระดับ type:

type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
type C = IsString<"hi">; // "yes" ("hi" extends string)

อ่าน T extends U ? X : Y ว่า “ถ้า T assignable ให้ U type จะเป็น X ไม่งั้นเป็น Yextends ตรงนี้คือการทดสอบ subset แบบเดียวกับในโมดูล foundations — ไม่ใช่ inheritance

conditional ซ้อนกันได้เพื่อสร้างการแตกกิ่งหลายทาง:

type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type T1 = TypeName<string>; // "string"
type T2 = TypeName<() => void>; // "function"
type T3 = TypeName<number[]>; // "object"

แต่ละ condition ถูกลองตามลำดับ เหมือน chain ของ else if

นี่คือพฤติกรรมที่ทำทุกคนงง เมื่อ type ที่ถูกตรวจเป็น naked type parameter และคุณส่ง union ให้ conditional จะ distribute ไปทีละ member แล้ว union ผลลัพธ์เข้าด้วยกัน:

type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>;
// ^? string[] | number[]

ผลลัพธ์ ไม่ ใช่ (string | number)[] แต่ conditional รันแยกกันสำหรับ string (ได้ string[]) และ number (ได้ number[]) แล้ว union กัน นี่คือพฤติกรรม distributive conditional type

flowchart TB
  input["ToArray ของ string หรือ number"] --> split["distribute ไปทีละ member"]
  split --> s["กิ่ง string: string array"]
  split --> n["กิ่ง number: number array"]
  s --> out["union ผลลัพธ์:
string array หรือ number array"]
  n --> out
A conditional distributes over each union member

การใช้จริงที่พบบ่อยคือการ filter union Exclude (built-in) คือสิ่งนี้เป๊ะ:

type MyExclude<T, U> = T extends U ? never : T;
type Colors = "red" | "green" | "blue";
type NoRed = MyExclude<Colors, "red">;
// ^? "green" | "blue"

member ที่ match U จะกลายเป็น never (และ never หายไปจาก union) ดังนั้นเหลือแค่ member ที่ไม่ match

บางครั้งคุณอยากตรวจ union ทั้งก้อน ไม่ใช่ทีละ member ห่อทั้งสองฝั่งด้วย tuple หนึ่งช่องเพื่อหยุด distribution — type parameter จะไม่ “naked” อีกต่อไป:

type IsUnionOfStrings<T> = [T] extends [string] ? true : false;
type D1 = IsUnionOfStrings<"a" | "b">; // true (tested as a whole)
// compare with the distributive version:
type Bad<T> = T extends string ? true : false;
type D2 = Bad<"a" | number>; // boolean (true | false — distributed!)
type D3 = IsUnionOfStrings<"a" | number>; // false (whole union isn't all string)

ทริก [T] extends [U] เป็น idiom มาตรฐาน — หยิบมาใช้เมื่อ conditional ผ่าน union ให้ผลเป็น boolean แบบแปลก ๆ (สัญญาณว่า distribute แล้ว union true | false เข้าด้วยกัน)

อ่าน `T extends U ? X : Y` อย่างไร?
`ToArray<string | number>` สำหรับ `type ToArray<T> = T extends any ? T[] : never` ได้อะไร?
built-in `Exclude<T, U>` เอา member ออกจาก union อย่างไร?
ทำไมต้องห่อทั้งสองฝั่งเป็น `[T] extends [U]`?