[FE] TypeScript 類型編程(小結(jié))

1. TypeScript 基本類型

TypeScript 有以下這些基本類型:string, number, boolean。
單個(gè)的值也是看做類型,1, 'a', null, true。

類型可以看做是值的 “集合”。
Effective TypeScript: (Item 7)Think of Types as Sets of Values

2. 構(gòu)造新類型

2.1 類型運(yùn)算

類型之間可以組合成新的類型,

  • 交集:&
  • 并集:|
  • 類型構(gòu)造器:數(shù)組 [], interface {}, 新的字符串類型 ${a}$

2.2 類型函數(shù)

可以借用泛型實(shí)現(xiàn)類型上的函數(shù),進(jìn)行類型變換,把入?yún)㈩愋?,轉(zhuǎn)換成出參類型,

type func<x extends string, y extends string> = {  // 如果只進(jìn)行類型編程,參數(shù)的大小寫就無所謂了
  result: [x, y, `${x}-${y}`]
};

a extends b 用來判斷 類型 a 是否 b 的子類型(子集),

  • 用于 類型函數(shù)的參數(shù)中,表示對類型參數(shù)進(jìn)行限制
  • 用于 類型函數(shù)體中,表示分支判斷(下文介紹)

可以用 ts-toolbelt 跑一下測試,

import { Test } from 'ts-toolbelt';
const { checks, check } = Test;

checks([
  check<  // 判斷兩個(gè)類型是否相等
    func<'a', 'b'>,
    {
      result: ['a', 'b', `a-b`]
    },
    Test.Pass
  >(),
]);

值得一提是,類型相等性判斷不同人可能會(huì)有不同做法,ts-toolbelt/Equals 中用的是,

type Equals<A1 extends any, A2 extends any> = 
  (<A>() => A extends A2 ? 1 : 0) extends (<A>() => A extends A1 ? 1 : 0) 
  ? 1 
  : 0;

參考 github issue: type level equal operator

3. 類型編程

我們知道編程語言的控制結(jié)構(gòu)包括三種:順序、選擇、循環(huán)。

  • 順序:使用 類型函數(shù)(上文介紹了)
  • 選擇:使用 extends
  • 循環(huán):使用 類型函數(shù) 的遞歸

3.1 分支判斷

在類型函數(shù)中,可以使用 extends 來實(shí)現(xiàn)分支判斷(使用 infer 可實(shí)現(xiàn)模式匹配)。

例如,

type func<str> = 
  str extends `${infer first}${infer tail}`
  ? [first, tail]
  : unknown
import { Test } from 'ts-toolbelt';
const { checks, check } = Test;

checks([
  check<  // 判斷兩個(gè)類型是否相等
    func<'abc'>,
    ['a', 'bc'],
    Test.Pass
  >(),
]);

3.1 循環(huán)(遞歸)

type array<head extends string, tail extends string[]>
  = [head, ...tail];

type join<strs extends string[], result extends string>
  = strs extends [] ? result
  : strs extends array<infer head, []> ? `${result}${head}`
  : strs extends array<infer head, infer tail> ? join<tail, `${result}${head}`>
  : never;
import { Test } from 'ts-toolbelt';
const { checks, check } = Test;

checks([
  check<
    join<['hello', ' ', 'world'], ''>,
    'hello world',
    Test.Pass
  >(),
]);

其中引入了輔助函數(shù) array,是為了限定 headtail 的類型。
否則會(huì)報(bào)以下錯(cuò)誤,

(1)`${result}${head}`
Type 'head' is not assignable to type 'string | number | bigint | boolean'.
  Type 'head' is not assignable to type 'number'.ts(2322)

(2)join<tail, `${result}${head}`>
Type 'tail' does not satisfy the constraint 'string[]'.
  Type 'unknown[]' is not assignable to type 'string[]'.
    Type 'unknown' is not assignable to type 'string'.ts(2344)
Type 'head' is not assignable to type 'string | number | bigint | boolean'.
  Type 'head' is not assignable to type 'number'.ts(2322)

出錯(cuò)的示例如下,

type join<strs extends string[], result extends string>
  = strs extends [] ? result
  : strs extends [infer head] ? `${result}${head}`
  : strs extends [infer head, ...infer tail] ? join<tail, `${result}${head}`>
  : never;

4. keyofin

在學(xué)習(xí) Mapped Types 的時(shí)候,
經(jīng)常會(huì)看到 keyofin 兩個(gè)操作符,曾經(jīng)造成過一些困擾,這里總結(jié)如下。

例子,

type func<input> = {
  [props in keyof input]: {  // 這里 keyof input 為:'a' | 'b'
    [field in props]: input[field]  // props 分別為 'a'(可以看做只有一個(gè)類型的 union) 和 'b'
  }
}
import { Test } from 'ts-toolbelt';
const { checks, check } = Test;

checks([
  check<
    func<{ a: number, b: string }>,
    { a: { a: number }, b: { b: string } },
    Test.Pass
  >(),
]);

這里有幾個(gè)值得注意的點(diǎn):

(1)通過 keyof 獲取屬性類型的 union

type func<obj> = keyof obj;  // 所有屬性名構(gòu)成的 union
import { Test } from 'ts-toolbelt';
const { checks, check } = Test;

checks([
  check<
    func<{ a: number, b: string }>,
    'a' | 'b',
    Test.Pass
  >(),
]);

(2)值類型的 union

type func<obj, k extends keyof obj> = obj[k];  // 獲取屬性值對應(yīng)值的 union【k 是一個(gè) union】
import { Test } from 'ts-toolbelt';
const { checks, check } = Test;

checks([
  check<
    func<{ a: number, b: string, c: boolean }, 'a' | 'c'>,
    number | boolean,
    Test.Pass
  >(),
]);

(3)使用 in 進(jìn)行循環(huán)操作

type func<obj, k extends keyof obj> = {
  [prop in k]: obj[prop]
};
import { Test } from 'ts-toolbelt';
const { checks, check } = Test;

checks([
  check<
    func<{ a: number, b: string, c: boolean }, 'a' | 'c'>,
    { a: number, c: boolean },
    Test.Pass
  >(),
]);

5. 內(nèi)置函數(shù)

TypeScript 內(nèi)置了一些 類型函數(shù),稱為 Utility Types
位于 typescript/lib/lib.es5.d.ts L1471-L1561

本地位置通常在這里,

/Applications/Visual Studio Code.app/Contents/Resources/app/extensions/node_modules/typescript/lib/lib.es5.d.ts

我們來學(xué)習(xí)一下這些內(nèi)置函數(shù)的實(shí)現(xiàn),

// 所有字段變成可選:{a: number, b?:string} -> {a?:number, b?:string}
// 已經(jīng)可選的不受影響
type Partial<T> = {
    [P in keyof T]?: T[P];
};

// 所有字段變成必填:{a?: number, b:string} -> {a:number, b:string}
// 已經(jīng)必填的不受影響
type Required<T> = {
    [P in keyof T]-?: T[P];
};

// 所有字段變成 readonly
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

// 過濾 interface T,只留下給定 prop
// Pick<{a:1,b:2,c:3}, 'a'|'c'> -> {a:1,c:3}
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

// 這里必須限定 K 是 `extends keyof any`,否則會(huì)報(bào)錯(cuò)
// Type 'K' is not assignable to type 'string | number | symbol'.
//  Type 'K' is not assignable to type 'symbol'.ts(2322)
// Record<'a'|'b', 1> -> {a:1,b:1}
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

// 計(jì)算差集 T-U
// 判斷 T 中(union)的每一個(gè)部分,是否是 U 的子類型,是就去掉,否則留下,最后將結(jié)果 union 起來
// Exclude<'a'|'b', 'b'|'c'> -> 'a'
type Exclude<T, U> = T extends U ? never : T;

// 計(jì)算 交集
// Extract<'a'|'b', 'b'|'c'> -> 'b'
type Extract<T, U> = T extends U ? T : never;

// 從 T 中去掉部分 props
// Omit<{a:1,b:2,c:3}, 'a'|'c'> -> {b:2}
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

// 約束 T 不能是 null 或 undefined
type NonNullable<T> = T extends null | undefined ? never : T;

// 通過模式匹配 infer,獲得函數(shù)的參數(shù)類型
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

// 獲取 constructor 的參數(shù)類型
// abstract 指的是 https://www.tutorialsteacher.com/typescript/abstract-class
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;

// 獲取函數(shù)的返回值類型
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

// 獲取構(gòu)造函數(shù)示例的類型
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;

// 將 S 轉(zhuǎn)換成大寫(intrinsic 表示需要 TypeScript 內(nèi)部來實(shí)現(xiàn))
type Uppercase<S extends string> = intrinsic;
// 將 S 轉(zhuǎn)換成小寫
type Lowercase<S extends string> = intrinsic;
// 將 S 轉(zhuǎn)換成 首字母大寫
type Capitalize<S extends string> = intrinsic;
// 將 S 轉(zhuǎn)換成首字母小寫
type Uncapitalize<S extends string> = intrinsic;

6. 加法運(yùn)算

type L<n extends number, r extends never[]>
  = r['length'] extends n ? r
  : L<n, [never, ...r]>

type add<x extends number, y extends number>
  = [...L<x, []>, ...L<y, []>]['length'];

type minus<x extends number, y extends number>
  = L<x, []> extends [...head: L<y, []>, ...tail: infer z] ? z['length']
  : never;

type mul<x extends number, y extends number>
  = y extends 0 ? 0
  : mul<x, minus<y, 1>> extends infer r
    ? r extends number ? add<r, x>  // 手工限定遞歸步驟的類型為 number
    : never
  : never;
import { Test } from 'ts-toolbelt';
const { checks, check } = Test;

checks([
  check<
    mul<2, 3>,  // 實(shí)現(xiàn)類型上的乘法
    6,
    Test.Pass
  >(),
]);

值得一提的是 mul 的實(shí)現(xiàn),以下實(shí)現(xiàn)方式會(huì)報(bào)錯(cuò),

type mul<x extends number, y extends number>
  = y extends 0 ? 0
  : add<mul<x, minus<y, 1>>, x>;  // Type instantiation is excessively deep and possibly infinite. ts(2589)

解決方案 參考 Type instantiation is excessively deep and possibly infinite. ts(2589)


參考

TypeScript Type-Level Programming
用 TypeScript 模板字面類型來制作 URL parser
用 TypeScript 類型運(yùn)算實(shí)現(xiàn)一個(gè)中國象棋程序
TypeScript 類型體操天花板,用類型運(yùn)算寫一個(gè) Lisp 解釋器

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容