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,是為了限定 head 和 tail 的類型。
否則會(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. keyof 和 in
在學(xué)習(xí) Mapped Types 的時(shí)候,
經(jīng)常會(huì)看到 keyof 和 in 兩個(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 解釋器