# 前置知识
# 类型是什么
类型即 number
、boolean
、string
等基础类型和 Object
、Function
等复合类型,它们是编程语言提供的对不同内容的抽象:
不同类型变量占据的内存大小不同:boolean
类型的变量会分配 4 个字节的内存,而 number
类型的变量则会分配 8 个字节的内存,给变量声明了不同的类型就代表了会占据不同的内存空间。
不同类型变量可做的操作不同:number
类型可以做加减乘除等运算,boolean
就不可以,复合类型中不同类型的对象可用的方法不同,比如 Date
和 RegExp
,变量的类型不同代表可以对该变量做的操作就不同。
如果能保证对某种类型只做该类型允许的操作,就叫做类型安全。类型检查目的是为了保证类型安全。
在运行时类型检查叫做动态类型检查,在编译时类型检查叫做静态类型检查。
# 类型系统
- 简单类型系统
- 变量、函数、类等都可以声明类型,编译器会基于声明的类型做类型检查
- 支持泛型的类型系统
- 声明时可以将变化的类型声明为泛型,编译器会根据传入的实际类型做类型检查
- 支持类型编程的类型系统
- 可以对传入的类型参数(泛型)做逻辑运算,产生新的类型
# TypeScript 类型系统中的类型
- JavaScript 的运行时类型
boolean
number
bigint
string
symbol
null
undefined
object
- 包装类型
Boolean
Number
BigInt
String
Symbol
Object
- 复合类型
class
function
array
- TypeScript 新增类型
Tuple
Enum
Interface
- 特殊类型
any
unknown
never
void
# TypeScript 类型系统中的类型运算
条件:T extends U ? X : Y
type IsString<T> = T extends string ? true : false;
推导:infer R
// 获取函数返回值类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
// 获取函数参数类型
type ParamType<T> = T extends (...args: infer P) => any ? P : any;
联合:T | U
type Union = string | number;
交叉:T & U
type Person = { name: string } & { age: number };
// 等价于
type Person = { name: string; age: number };
// 注意,不同类型的交叉不会合并
type res = string & number; // never
映射类型:{ [K in keyof T]: X }
type Person = { name: string; age: number };
type ReadonlyPerson = { readonly [K in keyof Person]: Person[K] };
keyof T
:获取 T 的所有属性名组成的联合类型T[K]
:获取 T 的属性 K 的类型in
:遍历
# 模式匹配
字符串使用正则做模式匹配:
代码语言:javascript复制const str = "hello world";
const reg = /hello (w )/;
const res = str.replace(reg, "Hi, $1"); // Hi, world
TypeScript 类型做模式匹配:
代码语言:javascript复制type CustomP = Promise<"Cell">;
type GetValueType<T> = T extends Promise<infer R> ? R : never;
type res = GetValueType<CustomP>; // Cell
const val: res = "Cell"; // ok
Typescript 类型的模式匹配是通过 extends
对类型参数做匹配,结果保存到通过 infer
声明的局部类型变量里,如果匹配就能从该局部变量里拿到提取出的类型。
# 数组类型
代码语言:javascript复制type arr = [number, string, boolean];
type GetFirst<Arr extends unknown[]> = Arr extends [infer First, ...unknown[]] ? First : never;
type GetLast<Arr extends unknown[]> = Arr extends [...unknown[], infer Last] ? Last : never;
type PopArr<Arr extends unknown[]> = Arr extends []
? []
: Arr extends [...infer Rest, unknown]
? Rest
: never;
type ShiftArr<Arr extends unknown[]> = Arr extends []
? []
: Arr extends [unknown, ...infer Rest]
? Rest
: never;
`any` 和 `unknown` 的区别
any
和 unknown
都代表任意类型,但是 any
是类型系统的顶级类型,可以赋值给任意类型,而 unknown
是类型系统的底级类型,不能赋值给任意类型,只能赋值给 any
或者 unknown
。
# 字符串类型
代码语言:javascript复制type str = "hello world";
type StartWith<Str extends string, Prefix extends string> = Str extends `${Prefix}${infer Rest}`
? true
: false;
type EndWith<Str extends string, Suffix extends string> = Str extends `${infer Rest}${Suffix}`
? true
: false;
type ReplaceStr<
Str extends string,
From extends string,
To extends string
> = Str extends `${Prefix}${From}${Suffix}` ? `${Prefix}${To}${Suffix}` : Str;
type TrimStrRight<Str extends string> = Str extends `${infer Rest}${" " | "t" | "n"}`
? TrimStrRight<Rest>
: Str; // 去除字符串右边的空格
type TrimStrLeft<Str extends string> = Str extends `${" " | "t" | "n"}${infer Rest}`
? TrimStrLeft<Rest>
: Str; // 去除字符串左边的空格
type TrimStr<Str extends string> = TrimStrRight<TrimStrLeft<Str>>; // 去除字符串两边的空格
# 函数
代码语言:javascript复制type GetParameters<Func extends Function> = Func extends (...args: infer Args) => unknown
? Args
: never;
type GetReturnType<Func extends Function> = Func extends (...args: any[]) => infer ReturnType
? ReturnType
: never;
# 构造器
代码语言:javascript复制interface Person {
name: string;
age: number;
}
interface PersonConstructor {
new (name: string, age: number): Person;
}
type GetInstanceType<ConstructorType extends new (...args: any) => any> =
ConstructorType extends new (...args: any) => infer InstanceType ? InstanceType : any;
type GetConstructorParameters<ConstructorType extends new (...args: any) => any> =
ConstructorType extends new (...args: infer ParametersType) => any ? ParametersType : never;
# 索引类型
代码语言:javascript复制type GetRefPropType<Props> = "ref" extends keyof Props
? Props extends { ref?: infer RefType | undefined }
? RefType
: never
: never;
TypeScript 类型的模式匹配是通过类型 extends
一个模式类型,把需要提取的部分放到通过 infer
声明的局部变量里,后面可以从这个局部变量拿到类型做各种后续处理。
# 重新构造
类型编程主要的目的就是对类型做各种转换,TypeScript 类型系统支持 3 种可以声明任意类型的变量: type
、infer
、类型参数。
// type 类型别名,声明一个变量存储某个类型
type P = Promise<string>;
// infer 用于类型的提取,然后存到一个变量里,相当于局部变量
type GetValueType<T> = T extends Promise<infer R> ? R : never;
// 类型参数用于接受具体的类型,在类型运算中也相当于局部变量
type isNumber<T> = T extends number ? true : false;
严格来说这三种也都不叫变量,因为它们不能被重新赋值。
TypeScript 的 type
、infer
、类型参数声明的变量都不能修改,想对类型做各种变换产生新的类型就需要重新构造。
# 数组类型
代码语言:javascript复制type tuple = [1, 2, 3];
type PushArr<Arr extends unknown[], Item> = [...Arr, Item]; // 在数组尾部添加元素
type PushResult = PushArr<tuple, 4>; // [1, 2, 3, 4]
type UnshiftArr<Arr extends unknown[], Item> = [Item, ...Arr]; // 在数组头部添加元素
type UnshiftResult = UnshiftArr<tuple, 0>; // [0, 1, 2, 3]
type tuple1 = [1, 2];
type tuple2 = ["hello", "world"];
type ZipArr<Arr1 extends [unknown, unknown], Arr2 extends [unknown, unknown]> = Arr1 extends [
infer OneFirst,
infer OneSecond
]
? Arr2 extends [infer TwoFirst, infer TwoSecond]
? [[OneFirst, TwoFirst], [OneSecond, TwoSecond]]
: []
: [];
type ZipResult = ZipArr<tuple1, tuple2>; // [[1, 'hello'], [2, 'world']]
# 字符串类型
字符串类型的重新构造:从已有的字符串类型中提取出一些部分字符串,经过一系列变换,构造成新的字符串类型。
代码语言:javascript复制type str = "hello world";
type CapitalizeStr<Str extends string> = Str extends `${infer First}${infer Rest}`
? `${Uppercase<First>}${Rest}`
: Str; // Uppercase 是内置类型,把字符串转换为大写
type CapitalizeResult = CapitalizeStr<str>; // "Hello world"
type str2 = "hello_world";
type CamelCaseStr<Str extends string> = Str extends `${infer Left}_${infer Right}${infer Rest}`
? `${Left}${Uppercase<Right>}${CamelCaseStr<Rest>}`
: Str;
type CamelCaseResult = CamelCaseStr<str2>; // "helloWorld"
type str3 = "helloWorld, helloWorld, helloTypeScript";
type DropSubStr<
Str extends string,
SubStr extends string
> = Str extends `${infer Left}${SubStr}${infer Right}`
? DropSubStr<`${Left}${Right}`, SubStr>
: Str;
type DropSubStrResult = DropSubStr<str3, "hello">; // "World, World, TypeScript"
# 函数类型
代码语言:javascript复制type Func = (a: number, b: string) => boolean;
type AppendArg<Func extends Function, Arg> = Func extends (...args: infer Args) => infer ReturnType
? (...args: [...Args, Arg]) => ReturnType
: never;
type AppendArgResult = AppendArg<Func, boolean>; // (a: number, b: string, c: boolean) => boolean
# 索引类型
索引类型是聚合多个元素的类型,class
、对象等都是索引类型。
type obj = {
name: string;
age: number;
};
// Mapping
type Mapping<Obj extends object> = {
[Key in keyof Obj]: [Obj[Key], Obj[Key], Obj[Key]];
};
type MappingResult = Mapping<obj>; // { name: [string, string, string]; age: [number, number, number] }
type UppercaseKey<Obj extends object> = {
[Key in keyof Obj as Uppercase<Key & string>]: Obj[Key];
}; // Key & string 约束 Key 必须是 string 类型
type UppercaseKeyResult = UppercaseKey<obj>; // { NAME: string; AGE: number }
// Record - TypeScript 提供了内置的高级类型 Record 来创建索引类型
// type Record<K extends string | number | symbol, T> = {
// [P in K]: T;
// };
type RecordResult = Record<"name" | "age", string>; // { name: string; age: string }
type ToReadonly<T> = {
readonly [P in keyof T]: T[P];
};
type ToReadonlyResult = ToReadonly<obj>; // { readonly name: string; readonly age: number }
type ToPartial<T> = {
[P in keyof T]?: T[P];
};
type ToPartialResult = ToPartial<obj>; // { name?: string; age?: number }
type ToMutable<T> = {
-readonly [P in keyof T]: T[P];
};
type ToMutableResult = ToMutable<ToReadonlyResult>; // { name: string; age: number }
type ToRequired<T> = {
[P in keyof T]-?: T[P];
};
type ToRequiredResult = ToRequired<ToPartialResult>; // { name: string; age: number }
type ToPick<T, K extends keyof T> = {
[P in K]: T[P];
};
type ToPickResult = ToPick<obj, "name">; // { name: string }
type FilterByValueType<Obj extends Record<string, any>, ValueType> = {
[Key in keyof obj as Obj[Key] extends ValueType ? Key : never]: Obj[Key];
};
type FilterByValueTypeResult = FilterByValueType<obj, string>; // { name: string }
TypeScript 支持 type
、infer
、类型参数
来保存任意类型,相当于变量的作用。
但其实也不能叫变量,因为它们是不可变的。想要变化就需要重新构造新的类型,并且可以在构造新类型的过程中对原类型做一些过滤和变换。
数组、字符串、函数、索引类型等都可以用这种方式对原类型做变换产生新的类型。其中索引类型有专门的语法叫做映射类型,对索引做修改的 as
叫做重映射。
# 递归复用
递归
递归是把问题分解为一系列相似的小问题,通过函数不断调用自身来解决这一个个小问题,直到满足结束条件,就完成了问题的求解。
TypeScript 的高级类型支持类型参数,可以做各种类型运算逻辑,返回新的类型,和函数调用是对应的,自然也支持递归。
TypeScript 类型系统不支持循环,但支持递归。当处理数量(个数、长度、层数)不固定的类型的时候,可以只处理一个类型,然后递归的调用自身处理下一个类型,直到结束条件也就是所有的类型都处理完了,就完成了不确定数量的类型编程,达到循环的效果。
# Promise
代码语言:javascript复制type p = Promise<Promise<Promise<Record<string, any>>>>;
type DeepPromiseValueType<P extends Promise<unknown>> = P extends Promise<infer ValueType>
? ValueType extends Promise<unknown>
? DeepPromiseValueType<ValueType>
: ValueType
: never;
type DeepPromiseValueTypeResult = DeepPromiseValueType<p>; // { [key: string]: any }
type DeepPromiseValueType2<P> = P extends Promise<infer ValueType>
? DeepPromiseValueType2<ValueType>
: P;
type DeepPromiseValueTypeResult2 = DeepPromiseValueType2<p>; // { [key: string]: any }
# 数组类型
代码语言:javascript复制type arr = [1, 2, 3, 4, 5];
type ReverseArr<Arr extends unknown[]> = Arr extends [infer Head, ...infer Tail]
? [...ReverseArr<Tail>, Head]
: Arr;
type ReverseArrResult = ReverseArr<arr>; // [5, 4, 3, 2, 1]
type IsEqual<X, Y> = [X] extends [Y] ? ([Y] extends [X] ? true : false) : false;
type ArrIncludes<Arr extends unknown[], Target> = Arr extends [infer Cursor, ...infer Rest]
? IsEqual<Cursor, Target> extends true
? true
: ArrIncludes<Rest, Target>
: false;
type ArrIncludesResult = ArrIncludes<arr, 3>; // true
type ArrIncludesResult2 = ArrIncludes<arr, 6>; // false
type ArrIncludesResult3 = ArrIncludes<[], 1>; // false
type RemoveItem<Arr extends unknown[], Target, Result extends unknown[] = []> = Arr extends [
infer Cursor,
...infer Rest
]
? IsEqual<Cursor, Target> extends true
? RemoveItem<Rest, Target, Result>
: RemoveItem<Rest, Target, [...Result, Cursor]>
: Result;
type RemoveItemResult = RemoveItem<arr, 3>; // [1, 2, 4, 5]
type RemoveItemResult2 = RemoveItem<[1, 1, 2, 1, 3], 1>; // [2, 3]
type BuildArray<
Length extends number,
Ele = unknown,
Result extends unknown[] = []
> = Result["length"] extends Length ? Result : BuildArray<Length, Ele, [...Result, Ele]>;
type BuildArrayResult = BuildArray<5, "a">; // ["a", "a", "a", "a", "a"]
# 字符串类型
代码语言:javascript复制type ReplaceStr<
Str extends string,
From extends string,
To extends string
> = Str extends `${infer Head}${From}${infer Tail}` ? `${Head}${To}${Tail}` : Str;
type ReplaceStrResult = ReplaceStr<"hello world", "world", "typescript">; // "hello typescript"
type ReplaceAll<
Str extends string,
From extends string,
To extends string
> = Str extends `${infer Head}${From}${infer Tail}`
? `${Head}${To}${ReplaceAll<Tail, From, To>}`
: Str;
type ReplaceAllResult = ReplaceAll<"hello world", "l", "L">; // "heLLo worLd"
type StringToUnion<Str extends string> = Str extends `${infer Head}${infer Tail}`
? Head | StringToUnion<Tail>
: never;
type StringToUnionResult = StringToUnion<"hello">; // "h" | "e" | "l" | "o"
type ReverseStr<
Str extends string,
Result extends string = ""
> = Str extends `${infer Head}${infer Tail}` ? ReverseStr<Tail, `${Head}${Result}`> : Result;
type ReverseStrResult = ReverseStr<"hello">; // "olleh"
# 对象类型
代码语言:javascript复制type obj = {
name: string;
address: {
city: string;
country: string;
};
say: () => void;
};
type DeepReadonly<Obj extends Record<string, any>> = {
readonly [Key in keyof Obj]: Obj[Key] extends Record<string, any>
? Obj[Key] extends Function
? Obj[Key]
: DeepReadonly<Obj[Key]>
: Obj[Key];
};
type DeepReadonlyResult = DeepReadonly<obj>;
// type DeepReadonlyResult = {
// readonly name: string;
// readonly address: DeepReadonly<{
// city: string;
// country: string;
// }>;
// readonly say: () => void;
// }
// 注意 address 的值并没有触发计算,因为 TS 的类型只有被用到才会被计算
type DeepReadonly2<Obj extends Record<string, any>> = Obj extends any
? {
readonly [Key in keyof Obj]: Obj[Key] extends Record<string, any>
? Obj[Key] extends Function
? Obj[Key]
: DeepReadonly2<Obj[Key]>
: Obj[Key];
}
: never;
type DeepReadonlyResult2 = DeepReadonly2<obj>;
// type DeepReadonlyResult2 = {
// readonly name: string;
// readonly address: {
// readonly city: string;
// readonly country: string;
// };
// readonly say: () => void;
// }
在 TypeScript 类型系统中的高级类型也同样支持递归,在类型体操中,遇到数量不确定的问题,要条件反射的想到递归。 比如数组长度不确定、字符串长度不确定、索引类型层数不确定等。
# 数值计算
TypeScript 类型系统中没有加减乘除运算符,但是可以通过构造不同的数组然后取 length
的方式来完成数值计算,把数值的加减乘除转化为对数组的提取和构造。
type num1 = [1, 2, 3, 4, 5]["length"]; // 5
type num2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]["length"]; // 10
# 数组长度实现加减乘除
代码语言:javascript复制type BuildArray<
Length extends number,
Ele = unknown,
Arr extends unknown[] = []
> = Arr["length"] extends Length ? Arr : BuildArray<Length, Ele, [...Arr, Ele]>;
type BuildArrayResult = BuildArray<5, "a">; // ["a", "a", "a", "a", "a"]
type num1 = BuildArrayResult["length"]; // 5
type Add<Num1 extends number, Num2 extends number> = [
...BuildArray<Num1>,
...BuildArray<Num2>
]["length"];
type num2 = Add<5, 10>; // 15
type Substract<Num1 extends number, Num2 extends number> = BuildArray<Num1> extends [
...arr1: BuildArray<Num2>,
...arr2: infer Rest
]
? Rest["length"]
: never;
type num3 = Substract<10, 5>; // 5
type Multiply<
Num1 extends number,
Num2 extends number,
Result extends unknown[] = []
> = Num2 extends 0
? Result["length"]
: Multiply<Num1, Substract<Num2, 1>, [...Result, ...BuildArray<Num1>]>;
type num4 = Multiply<5, 10>; // 50
type Divide<Num1 extends number, Num2 extends number, Count extends unknown[] = []> = Num1 extends 0
? Count["length"]
: Divide<Substract<Num1, Num2>, Num2, [...Count, 1]>;
type num5 = Divide<10, 5>; // 2
type num6 = Divide<2, 5>; // never
// add(num1, num2) {
// return num1 num2;
// }
// substract(num1, num2) {
// return num1 - num2;
// }
// multiply(num1, num2, res = 0) {
// if (num2 === 0) {
// return res;
// }
// return multiply(num1, num2 - 1, res num1);
// }
// divide(num1, num2, count = 0) {
// if (num1 === 0 || num1 < num2) {
// return count;
// }
// return divide(num1 - num2, num2, count 1);
// }
# 数组长度实现计数
代码语言:javascript复制type StrLen<Str extends string, Count extends unknown[] = []> = Str extends `${string}${infer Rest}`
? StrLen<Rest, [...Count, 1]>
: Count["length"];
type StrLenResult = StrLen<"hello">; // 5
type GreaterThan<
Num1 extends number,
Num2 extends number,
Count extends unknown[] = []
> = Num1 extends Num2
? false
: Count["length"] extends Num2
? true
: Count["length"] extends Num1
? false
: GreaterThan<Num1, Num2, [...Count, 1]>;
type isGreater1 = GreaterThan<5, 10>; // false
type isGreater2 = GreaterThan<10, 5>; // true
// greaterThan(num1, num2, count = 0) {
// if (num1 === num2) {
// return false;
// }
// if (count === num2) {
// return true;
// }
// if (count === num1) {
// return false;
// }
// return greaterThan(num1, num2, count 1);
// }
# 联合类型
当类型参数为联合类型,并且在条件类型左边直接引用该类型参数的时候,TypeScript 会把每一个元素单独传入来做类型运算,最后再合并成联合类型,这种语法叫做分布式条件类型。
代码语言:javascript复制type Union = "a" | "b" | "c";
type UppercaseItem<Items extends string, Target extends string> = Items extends Target
? Uppercase<Items>
: Items;
type UppercaseUnion = UppercaseItem<Union, "a">; // "A" | "b" | "c"
type UppercaseUnion2 = UppercaseItem<Union, "b">; // "a" | "B" | "c"
type AddPrefix<Items extends string, Prefix extends string> = `${Prefix}${Items}`;
type AddPrefixUnion = AddPrefix<Union, "prefix_">; // "prefix_a" | "prefix_b" | "prefix_c"
TypeScript 对联合类型在条件类型中使用时的特殊处理:会把联合类型的每一个元素单独传入做类型计算,最后合并。
# CamelcaseUnion
代码语言:javascript复制type CamelCaseStr<Str extends string> = Str extends `${infer Left}_${infer Right}${infer Rest}`
? `${Left}${Uppercase<Right>}${CamelCaseStr<Rest>}`
: Str;
type CamelCaseStrResult = CamelCaseStr<"hello_world">; // "helloWorld"
type CamelcaseArr<Arr extends unknown[]> = Arr extends [infer First, ...infer Rest]
? [CamelCaseStr<First & string>, ...CamelcaseArr<Rest>]
: Arr;
type CamelcaseArrResult = CamelcaseArr<["hello_world", "hello_ts"]>; // ["helloWorld", "helloTs"]
type CamelcaseUnion<Item extends string> = Item extends `${infer Left}_${infer Right}${infer Rest}`
? `${Left}${Uppercase<Right>}${CamelcaseUnion<Rest>}`
: Item;
type CamelcaseUnionResult = CamelcaseUnion<"hello_world" | "hello_ts">; // "helloWorld" | "helloTs"
# IsUnion
代码语言:javascript复制type IsUnion<A, B = A> = A extends A ? ([B] extends [A] ? false : true) : never;
type IsUnionResult = IsUnion<"a" | "b">; // true
type IsUnionResult2 = IsUnion<"a">; // false
条件类型中如果左边的类型是联合类型,会把每个元素单独传入做计算,而右边不会。
代码语言:javascript复制type TestUnion<A, B = A> = A extends A ? { a: A; b: B } : never;
type TestUnionResult = TestUnion<"a" | "b" | "c">;
// type TestUnionResult = {
// a: "a";
// b: "a" | "b" | "c";
// } | {
// a: "b";
// b: "a" | "b" | "c";
// } | {
// a: "c";
// b: "a" | "b" | "c";
// }
当 `A` 是联合类型时
A extends A
这种写法是为了触发分布式条件类型,让每个类型单独传入处理的,没别的意义。A extends A
和[A] extends [A]
是不同的处理,前者是单个类型和整个类型做判断,后者两边都是整个联合类型,因为只有extends
左边直接是类型参数才会触发分布式条件类型。
# BEM
BEM 是 css 命名规范,用 block__element--modifier
的形式来描述某个区块下面的某个元素的某个状态的样式。
type BEM<
Block extends string,
Element extends string[],
Modifier extends string[]
> = `${Block}__${Element[number]}--${Modifier[number]}`;
type bemResult = BEM<"msg", ["title", "content"], ["red", "bold"]>;
// type bemResult = "msg__title--red"
// | "msg__title--bold"
// | "msg__content--red"
// | "msg__content--bold"
# AllCombinations
实现一个全组合的高级类型,传入 'A' | 'B'
的时候,能够返回所有的组合: 'A' | 'B' | 'BA' | 'AB'
。
type Combination<A extends string, B extends string> = A | B | `${A}${B}` | `${B}${A}`;
type CombinationResult = Combination<"A", "B">;
// "A" | "B" | "AB" | "BA"
type AllCombinations<A extends string, B extends string = A> = A extends A
? Combination<A, AllCombinations<Exclude<B, A>>>
: never;
type AllCombinationsResult = AllCombinations<"A" | "B" | "C">;
// "A" | "B" | "C" | "AB" | "AC" | "BA" | "BC" | "CA" | "CB" | "ABC" | "ACB" | "BAC" | "BCA" | "CAB" | "CBA"
联合类型中的每个类型都是相互独立的,TypeScript 对它做了特殊处理,也就是遇到字符串类型、条件类型的时候会把每个类型单独传入做计算,最后把每个类型的计算结果合并成联合类型。
# 特殊类型
类型的判断要根据它的特性来,比如判断联合类型就要根据它的 distributive
的特性。
# IsAny
any
类型与任何类型的交叉都是 any
,也就是 1 & any
结果是 any
。
type IsAny<T> = 0 extends 1 & T ? true : false;
type IsAnyResult = IsAny<any>; // true
type IsAnyResult2 = IsAny<unknown>; // false
# IsEqual
代码语言:javascript复制type IsEqual<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
? true
: false;
type IsEqualResult = IsEqual<"a", "a">; // true
type IsEqualResult2 = IsEqual<"a", "b">; // false
type IsEqualResult3 = IsEqual<any, "a">; // false
type IsEqualResult4 = IsEqual<any, any>; // true ??
# IsUnion
判断 union
类型,要根据它遇到条件类型时会分散成单个传入做计算的特性:
type IsUnion<A, B = A> = A extends A ? ([B] extends [A] ? false : true) : never;
type IsUnionResult = IsUnion<"a" | "b">; // true
type IsUnionResult2 = IsUnion<"a">; // false
# IsNever
never
在条件类型中也比较特殊,如果条件类型左边是类型参数,并且传入的是 never
,那么直接返回 never
:
type TestNever<T> = T extends number ? 1 : 2;
type TestNeverResult = TestNever<never>; // never
type IsNever<T> = [T] extends [never] ? true : false;
type IsNeverResult = IsNever<never>; // true
# IsTuple
元组类型的 length
是数字字面量,而数组的 length
是 number
。
type NotEqual<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
? false
: true;
type IsTuple<T> = T extends [...params: infer Eles] ? NotEqual<Eles["length"], number> : false;
type IsTupleResult = IsTuple<[1, 2, 3]>; // true
type IsTupleResult2 = IsTuple<number[]>; // false
# UnionToIntersection
类型之间是有父子关系的,更具体的那个是子类型,比如 A
和 B
的交叉类型 A & B
就是联合类型 A | B
的子类型,因为更具体。
如果允许父类型赋值给子类型,就叫做逆变。
如果允许子类型赋值给父类型,就叫做协变。
在 TypeScript 中有函数参数是有逆变的性质的,也就是如果参数可能是多个类型,参数类型会变成它们的交叉类型。
代码语言:javascript复制type UnionToIntersection<U> = (U extends U ? (x: U) => unknown : never) extends (
x: infer R
) => unknown
? R
: never;
// U extends U 是为了触发联合类型的 distributive 的性质,让每个类型单独传入做计算,最后合并
type UnionToIntersectionResult = UnionToIntersection<
| {
name: string;
}
| {
age: number;
}
>;
// { name: string; } & { age: number; }
# GetOptional
如何提取索引类型中的可选索引呢?利用可选索引的特性:可选索引的值为 undefined
和值类型的联合类型。
type GetOptional<Obj extends Record<string, any>> = {
[Key in keyof Obj as {} extends Pick<Obj, Key> ? Key : never]: Obj[Key];
};
type GetOptionalResult = GetOptional<{
name: string;
age?: number;
}>;
// type GetOptionalResult = {
// age?: number | undefined;
// }
用映射类型的语法重新构造索引类型,索引是之前的索引也就是 Key in keyof Obj
,但要做一些过滤,也就是 as
之后的部分。
Pick
是 ts 提供的内置高级类型,就是取出某个 Key
构造新的索引类型:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
可选的意思是这个索引可能没有,没有的时候,那 Pick<Obj, Key>
就是空的,所以 {} extends Pick<Obj, Key>
就能过滤出可选索引。
# GetRequired
代码语言:javascript复制type isRequired<Key extends keyof Obj, Obj> = {} extends Pick<Obj, Key> ? never : Key;
type isRequiredResult = isRequired<"name", { name: string; age?: number }>; // "name"
type GetRequired<Obj extends Record<string, any>> = {
[Key in keyof Obj as isRequired<Key, Obj>]: Obj[Key];
};
type GetRequiredResult = GetRequired<{
name: string;
age?: number;
}>;
// type GetRequiredResult = {
// name: string;
// }
# RemoveIndexSignature
索引类型可能有索引,也可能有可索引签名:
代码语言:javascript复制type Person = {
[key: string]: any; // 可索引签名,即可以添加任意个 string 类型的索引
say(): void; // 具体索引
};
索引签名不能构造成字符串字面量类型,因为它没有名字,而其他索引可以。
代码语言:javascript复制type RemoveIndexSignature<Obj extends Record<string, any>> = {
[Key in keyof Obj as Key extends `${infer Str}` ? Str : never]: Obj[Key];
};
type RemoveIndexSignatureResult = RemoveIndexSignature<{
[key: string]: any;
say(): void;
}>;
// type RemoveIndexSignatureResult = {
// say: () => void;
// }
# ClassPublicProps
如何过滤出 class
的 public
的属性呢?
根据它的特性:keyof
只能拿到 class
的 public
索引,private
和 protected
的索引会被忽略。
class Person {
public name: string;
protected id: number;
private age: number;
constructor() {
this.name = "Cell";
this.age = 18;
this.id = 1;
}
}
type ClassPublicProps<Obj extends Record<string, any>> = {
[Key in keyof Obj]: Obj[Key];
};
type ClassPublicPropsResult = ClassPublicProps<Person>;
// type ClassPublicPropsResult = {
// name: string;
// }
# as const
TypeScript 默认推导出来的类型并不是字面量类型:
代码语言:javascript复制const obj = {
age: 18,
};
type objType = typeof obj;
// type objType = {
// age: number;
// }
const arr = [1, 2, 3];
type arrType = typeof arr;
// type arrType = number[]
但是类型编程很多时候是需要推导出字面量类型的,这时候就需要用 as const
:
const obj = {
age: 18,
} as const;
type objType = typeof obj;
// type objType = {
// readonly age: 18;
// }
const arr = [1, 2, 3] as const;
type arrType = typeof arr;
// type arrType = readonly [1, 2, 3]
加上 as const
之后推导出来的类型是带有 readonly
修饰的,所以再通过模式匹配提取类型的时候也要加上 readonly
的修饰才行。
const
是常量的意思,也就是说这个变量首先是一个字面量值,而且还不可修改,有字面量和 readonly
两重含义。所以加上 as const
会推导出 readonly
的字面量类型。
# 特殊类型特性
any
类型与任何类型的交叉都是any
,也就是1 & any
结果是any
,可以用这个特性判断any
类型。- 联合类型作为类型参数出现在条件类型左侧时,会分散成单个类型传入,最后合并。
never
作为类型参数出现在条件类型左侧时,会直接返回never
。any
作为类型参数出现在条件类型左侧时,会直接返回trueType
和falseType
的联合类型。- 元组类型也是数组类型,但
length
是数字字面量,而数组的length
是number
。可以用来判断元组类型。 - 函数参数处会发生逆变,可以用来实现联合类型转交叉类型。
- 可选索引的索引可能没有,那
Pick
出来的就可能是{}
,可以用来过滤可选索引,反过来也可以过滤非可选索引。 - 索引类型的索引为字符串字面量类型,而可索引签名不是,可以用这个特性过滤掉可索引签名。
keyof
只能拿到class
的public
的索引,可以用来过滤出public
的属性。- 默认推导出来的不是字面量类型,加上
as const
可以推导出字面量类型,但带有readonly
修饰,这样模式匹配的时候也得加上readonly
才行。