Desmond

Desmond

An introvert who loves web programming, graphic design and guitar
github
bilibili
twitter

TypeScript 邏輯運算

條件類型#

type LiteralType<T> = T extends string ? "string" : "other";

type Res1 = LiteralType<"linbudu">; // "string"
type Res2 = LiteralType<599>; // "other"
export type LiteralType<T> = T extends string
	? "string"
	: T extends number
	? "number"
	: T extends boolean
	? "boolean"
	: T extends null
	? "null"
	: T extends undefined
	? "undefined"
	: never;

type Res1 = LiteralType<"linbudu">; // "string"
type Res2 = LiteralType<599>; // "number"
type Res3 = LiteralType<true>; // "boolean"

條件類型中使用 extends 判斷類型的相容性,而非判斷類型的全等性。這是因為在類型層面中,對於能夠進行賦值操作的兩個變數,我們並不需要它們的類型完全相等只需要具有相容性,而兩個完全相同的類型,其 extends 自然也是成立的。

function universalAdd<T extends number | bigint | string>(
	x: T,
	y: T
): LiteralToPrimitive<T> {
	return x + (y as any);
}

export type LiteralToPrimitive<T> = T extends number
	? number
	: T extends bigint
	? bigint
	: T extends string
	? string
	: never;

universalAdd("linbudu", "599"); // string
universalAdd(599, 1); // number
universalAdd(10n, 10n); // bigint
type Func = (...args: any[]) => any;

type FunctionConditionType<T extends Func> = T extends (
  ...args: any[]
) => string
  ? 'A string return func!'
  : 'A non-string return func!';

//  "A string return func!"
type StringResult = FunctionConditionType<() => string>;
// 'A non-string return func!';
type NonStringResult1 = FunctionConditionType<() => boolean>;
// 'A non-string return func!';
type NonStringResult2 = FunctionConditionType<() => number>;

infer 關鍵字#

TypeScript 中支持通過 infer 關鍵字來在條件類型中提取類型的某一部分信息,比如上面我們要提取函數返回值類型的話,可以這麼放:

type FunctionReturnType<T extends Func> = T extends (
  ...args: any[]
) => infer R
  ? R
  : never;

infer 是 inference 的縮寫,意為推斷,如 infer RR 就表示 待推斷的類型。 infer 只能在條件類型中使用,因為我們實際上仍然需要類型結構是一致的,比如上例中類型信息需要是一個函數類型結構,我們才能提取出它的返回值類型。如果連函數類型都不是,那我只會給你一個 never 。

這裡的類型結構當然並不局限於函數類型結構,還可以是數組:

type Swap<T extends any[]> = T extends [infer A, infer B] ? [B, A] : T;

type SwapResult1 = Swap<[1, 2]>; // 符合元組結構,首尾元素替換[2, 1]
type SwapResult2 = Swap<[1, 2, 3]>; // 不符合結構,沒有發生替換,仍是 [1, 2, 3]
// 提取首尾兩個
type ExtractStartAndEnd<T extends any[]> = T extends [
  infer Start,
  ...any[],
  infer End
]
  ? [Start, End]
  : T;

// 調換首尾兩個
type SwapStartAndEnd<T extends any[]> = T extends [
  infer Start,
  ...infer Left,
  infer End
]
  ? [End, ...Left, Start]
  : T;

// 調換開頭兩個
type SwapFirstTwo<T extends any[]> = T extends [
  infer Start1,
  infer Start2,
  ...infer Left
]
  ? [Start2, Start1, ...Left]
  : T;
type ArrayItemType<T> = T extends Array<infer ElementType> ? ElementType : never;

type ArrayItemTypeResult1 = ArrayItemType<[]>; // never
type ArrayItemTypeResult2 = ArrayItemType<string[]>; // string
type ArrayItemTypeResult3 = ArrayItemType<[string, number]>; // string | number
// 提取對象的屬性類型
type PropType<T, K extends keyof T> = T extends { [Key in K]: infer R }
  ? R
  : never;

type PropTypeResult1 = PropType<{ name: string }, 'name'>; // string
type PropTypeResult2 = PropType<{ name: string; age: number }, 'name' | 'age'>; // string | number

// 反轉鍵名與鍵值
type ReverseKeyValue<T extends Record<string, unknown>> = T extends Record<infer K, infer V> ? Record<V & string, K> : never

type ReverseKeyValueResult1 = ReverseKeyValue<{ "key": "value" }>; // { "value": "key" }
type PromiseValue<T> = T extends Promise<infer V> ? PromiseValue<V> : T;

type PromiseValueResult1 = PromiseValue<Promise<number>>; // number
type PromiseValueResult2 = PromiseValue<number>; // number,但並沒有發生提取

分佈式條件類型 (Distributive Conditional Type)#

對於屬於裸類型參數的檢查類型,條件類型會在實例化時期自動分發到聯合類型上。(是否通過泛型參數傳入)

type Condition<T> = T extends 1 | 2 | 3 ? T : never;

// 1 | 2 | 3
type Res1 = Condition<1 | 2 | 3 | 4 | 5>;

// never
type Res2 = 1 | 2 | 3 | 4 | 5 extends 1 | 2 | 3 ? 1 | 2 | 3 | 4 | 5 : never;
type Naked<T> = T extends boolean ? "Y" : "N";
type Wrapped<T> = [T] extends [boolean] ? "Y" : "N";

// "N" | "Y"
type Res3 = Naked<number | boolean>;

// "N"
type Res4 = Wrapped<number | boolean>;

把上面的線索理一下,其實我們就大致得到了條件類型分佈式起作用的條件。首先,你的類型參數需要是一個聯合類型。其次,類型參數需要通過泛型參數的方式傳入,而不能直接進行條件類型判斷(如 Res2 中)。最後,條件類型中的泛型參數不能被包裹。

而條件類型分佈式特性會產生的效果也很明顯了,即將這個聯合類型拆開來,每個分支分別進行一次條件類型判斷,再將最後的結果合併起來(如 Naked 中)。

而這裡的裸類型參數,其實指的就是泛型參數是否完全裸露,我們上面使用數組包裹泛型參數只是其中一種方式,比如還可以這麼做:

export type NoDistribute<T> = T & {};

type Wrapped<T> = NoDistribute<T> extends boolean ? "Y" : "N";

type Res1 = Wrapped<number | boolean>; // "N"
type Res2 = Wrapped<true | false>; // "Y"
type Res3 = Wrapped<true | false | 599>; // "N"

需要注意的是,我們並不是只會通過裸露泛型參數,來確保分佈式特性能夠發生。在某些情況下,我們也會需要包裹泛型參數來禁用掉分佈式特性。最常見的場景也許還是聯合類型的判斷,即我們不希望進行聯合類型成員的分佈判斷,而是希望直接判斷這兩個聯合類型的相容性判斷,就像在最初的 Res2 中那樣:

type CompareUnion<T, U> = [T] extends [U] ? true : false;

type CompareRes1 = CompareUnion<1 | 2, 1 | 2 | 3>; // true
type CompareRes2 = CompareUnion<1 | 2, 1>; // false

另外一種情況則是,當我們想判斷一個類型是否為 never 時,也可以通過類似的手段:

type IsNever<T> = [T] extends [never] ? true : false;

type IsNeverRes1 = IsNever<never>; // true
type IsNeverRes2 = IsNever<"linbudu">; // false
type IsAny<T> = 0 extends 1 & T ? true : false;

交叉類型就像短板效應一樣,其最終計算的類型是由最短的那根木板,也就是最精確的那個類型決定的

type IsUnknown<T> = unknown extends T
  ? IsAny<T> extends true
    ? false
    : true
  : false;

需要注意的是這裡的 never 與 any 的情況並不完全相同,any 在直接作為判斷參數時作為泛型參數時都會產生這一效果:

// 直接使用,返回聯合類型
type Tmp1 = any extends string ? 1 : 2;  // 1 | 2

type Tmp2<T> = T extends string ? 1 : 2;
// 透過泛型參數傳入,同樣返回聯合類型
type Tmp2Res = Tmp2<any>; // 1 | 2

// 如果判斷條件是 any,那麼仍然會進行判斷
type Special1 = any extends any ? 1 : 2; // 1
type Special2<T> = T extends any ? 1 : 2;
type Special2Res = Special2<any>; // 1

而 never 僅在作為泛型參數時才會產生:

// 直接使用,仍然會進行判斷
type Tmp3 = never extends string ? 1 : 2; // 1

type Tmp4<T> = T extends string ? 1 : 2;
// 透過泛型參數傳入,會跳過判斷
type Tmp4Res = Tmp4<never>; // never

// 如果判斷條件是 never,還是僅在作為泛型參數時才跳過判斷
type Special3 = never extends never ? 1 : 2; // 1
type Special4<T> = T extends never ? 1 : 2;
type Special4Res = Special4<never>; // never

References:
https://juejin.cn/book/7086408430491172901

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。