这篇文章总结了在做 TypeScript 类型挑战(type-challenges)题目过程中积累的一些技巧和知识点,便于后续查阅和复习。
类型操作符
keyof
keyof
操作符接受一个对象类型作为参数,返回该对象属性名组成的字面量联合类型
ts
type MyObj = {
foo: number;
bar: string;
};
type Keys = keyof MyObj; // 'foo'|'bar'
由于 JavaScript
对象的键名只有三种类型,所以对于any
的键名的联合类型就是 string|number|symbol
ts
// string | number | symbol
type KeyT = keyof any;
对于没有自定义键名的类型使用 keyof
运算符,返回never
类型,表示不可能有这样类型的键名
ts
type KeyT = keyof object; // never
对于联合类型,keyof
返回成员共有的键名
ts
type A = { a: string; z: boolean };
type B = { b: string; z: boolean };
// 返回 'z'
type KeyT = keyof (A | B);
对于交叉类型,keyof
返回所有键名
ts
type A = { a: string; x: boolean };
type B = { b: string; y: number };
// 返回 'a' | 'x' | 'b' | 'y'
type KeyT = keyof (A & B);
// 相当于
keyof (A & B) ≡ keyof A | keyof B
它的用法主要有两个:
用法一:与extends
一起使用,对对象属性的类型做限定
比如:
ts
function prop<Obj, K extends keyof Obj>(obj: Obj, key: K): Obj[K] {
return obj[key];
}
上面代码对函数prop
的参数做了规定:需要传两个参数,并且第二个参数key
必须是第一个参数obj
的属性值
这样使用不会报错:
ts
prop({ a: 1 }, "a");
但是这样使用就会报错了:
ts
prop({ a: 1 }, "b"); //Argument of type '"b"' is not assignable to parameter of type '"a"'.(2345)
用法二:与in
搭配进行属性映射,将一个类型的所有属性逐一映射成其他值
比如:
ts
type NewProps<Obj> = {
[Prop in keyof Obj]: boolean;
};
// 用法
type MyObj = { foo: number; bar: string };
// 等于 { foo: boolean; bar: boolean }
type NewObj = NewProps<MyObj>;
NewProps
类型可以将传入类型对象Obj
的所有属性值类型都转化为boolean
in
in
的右侧一般会跟一个联合类型,使用 in
操作符可以对该联合类型进行迭代。 其作用类似 JS
中的 for...in
或者 for...of
ts
type Animals = "pig" | "cat" | "dog";
type animals = {
[key in Animals]: string;
};
// type animals = {
// pig: string; //第一次迭代
// cat: string; //第二次迭代
// dog: string; //第三次迭代
// }
typeof
typeof
操作符用于获取一个 JavaScript
变量的类型,常用于获取一个普通对象或者一个函数的类型
基本使用
假如我们在定义类型之前已经有了对象 obj
,就可以用 typeof
来定义一个类型
ts
const p = {
name: "CJ",
age: 18,
};
type Person = typeof p;
// 等同于
type Person = {
name: string;
age: number;
};
获取嵌套对象类型
如果对象是一个嵌套的对象,typeof
也能够正确获取到它们的类型。
ts
const p = {
name: "CJ",
age: 18,
address: {
city: "SH",
},
};
type Person = typeof p;
// 相当于
type Person = {
name: string;
age: number;
address: {
city: string;
};
};
获取数组类型
假如我们有一个字符串数组,可以把数组的所有元素组合成一个新的类型:
ts
const data = ["hello", "world"] as const;
type Greeting = (typeof data)[number];
// type Greeting = "hello" | "world"
获取函数类型
函数也是一个特殊的对象,typeof
当然也能获取函数类型
ts
function add(a: number, b: number): number {
return a + b;
}
type AddFn = typeof add;
// AddFn: (a: number, b: number) => number
typeof
和 keyof
的区别:
关键字 | 作用 | 用法 | 结果类型 |
---|---|---|---|
typeof |
获取"值"的类型 | typeof 变量名 | 一个类型 |
keyof |
获取"类型"的所有键名 | keyof 类型名 | 联合类型(键名) |
extends
extends
关键词一般有两种用法:条件类型 和类型约束
条件类型
条件类型 类似于 JavaScript
中的三元表达式,可以根据当前类型是否符合某种条件,返回不同的类型
ts
T extends U ? X : Y
上面式子中的extends
用来判断,类型T
是否可以赋值给类型U
,即T
是否为U
的子类型,这里的T
和U
可以是任意类型。
举个例子:
ts{1,2}
type IsBoolean<T> = T extends boolean ? true : false
type IsArray<T> = T extends { length: number } ? true : false
type Res1 = IsBoolean<string> // false
type Res2 = IsBoolean<true> // true
type Res3 = IsBoolean<true> // false
type Res4 = IsArray<[1, 2]> // true
条件类型还有一种用法:当只有extends
右侧有联合类型,左侧没有联合类型时,虽然确实会进行多次判断,但是其多次判断的结果会以或的方式合并后交由extends
的逻辑处理
举个例子:
ts
'a' extends 'a' | 'b' ? 1 : 2
具体的流程
a extends a
返回为true
a extends b
返回为false
- 两个结果进行或判断即
true | false
为true
,所以最终结果返回1
分布式条件类型
在条件类型中有一个特别需要注意的东西就是:分布式条件类型 (对联合类型应用 extends
时,会遍历联合类型成员并一一应用该条件类型)
ts
// 内置工具:交集
type Extract<T, U> = T extends U ? T : never;
type type1 = "name" | "age";
type type2 = "name" | "address" | "sex";
type test = Extract<type1, type2>;
// 结果为 'name'
代码详解:
T extends U ? T : never
:因为T
是一个联合类型,所以这里适用于分布式条件类型的概念。根据其概念,在实际的过程中会把T
类型中的每一个子类型进行迭代
ts
// 初始状态
'name' | 'age' extends 'name' | 'address' | 'sex' ? T : never
// 第一次迭代使用'name',得到 'name'
'name' extends 'name' | 'address' | 'sex' ? 'name' : never
// 第二次迭代使用`age`,得到 never
'age' extends 'name' | 'address' | 'sex' : never
- 在迭代完成之后,会把每次迭代的结果组合成一个新的联合类型(根据
never
类型的特点,最后的结果会剔除掉never
)
ts
type result = "name" | never;
// 实际为 type result = 'name'
infer
infer
关键词的作用是延时推导 ,它会在类型未推导时进行占位,等到真正推导成功后再返回正确的类型,它用来定义泛型里面推断出的类型参数,它通常跟条件运算符一起使用,用在extends
关键字后面的父类型之中
概念有点难懂,这里举几个例子来说明下:
第一个例子我们看infer
在数组上的应用
ts
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
上面示例中,infer Item
表示Item
这个参数是 TypeScript
自己推断出来的,不用显式传入,而Flatten<Type>
则表示Type
这个类型参数是外部传入的。Type extends Array<infer Item>
则表示,如果参数Type
是一个数组,那么就将该数组的成员类型推断为Item
并返回Item
,即Item
是从Type
推断出来的
一旦使用Infer Item
定义了Item
,后面的代码就可以直接调用Item
了。下面是上例的泛型Flatten<Type>
的用法
typescript
// string
type Str = Flatten<string[]>;
// number
type Num = Flatten<number>;
上面示例中,第一个例子Flatten<string[]>
传入的类型参数是string[]
,可以推断出Item
的类型是string
,所以返回的是string
。第二个例子Flatten<number>
传入的类型参数是number
,它不是数组,所以直接返回自身
第二个例子我们看 infer
在函数上面的应用
typescript
type ReturnPromise<T> = T extends (...args: infer A) => infer R ? (...args: A) => Promise<R> : T;
上面示例中,如果T
是函数,就返回这个函数的 Promise
版本,否则原样返回。infer A
表示推断该函数的参数类型为A
,infer R
表示推断该函数的返回值类型为R
第三个例子我们看 infer
在对象上面的应用
typescript
type MyType<T> = T extends {
a: infer M;
b: infer N;
}
? [M, N]
: never;
// 用法示例
type T = MyType<{ a: string; b: number }>;
// [string, number]
上面实例中,表示如果T
有a
和b
两个属性,就把他们的类型M
、N
提取出来
相关参考资料:
修饰符
- public:公共修饰符,可以被类的实例、子类和外部访问。默认情况下,类的成员都是公共的。
- private:私有修饰符,只能被类的内部访问。私有成员对于外部是不可见的,子类也无法访问。
- protected:受保护修饰符,可以被类的内部和子类访问,对于外部是不可见的。
- readonly:只读修饰符,表示成员只能在声明时或构造函数内部被赋值,之后不可修改。
- static:静态修饰符,用于定义类级别的成员,而不是实例级别的成员。静态成员可以通过类名直接访问,而不需要创建实例。
- abstract:抽象修饰符,用于声明抽象类和抽象方法。抽象类不能被实例化,只能被继承,并且子类必须实现抽象方法。
as const
as const
是一种特殊的类型断言,它告诉Typescript
编译器:
- 将这个值视为常量(const),禁止重新赋值
- 推导出最精准的字面量类型
第一个特性还比较容易理解:
ts
let arr = [1, 2, 3, 4] as const;
arr = [1]; //Type '[1]' is not assignable to type 'readonly [1, 2, 3, 4]'.
被as const
断言过的变量是不能被更改的
第二个特性有点难理解,它的意思是as const
会自动推导出最精准的字面量类型,而不是泛化的类型(如string
、number[]
),看下面这个例子:
ts
let tuple = ["tesla", "model 3", "model X", "model Y"] as const;
let tuple2 = ["tesla", "model 3", "model X", "model Y"];
type tupleType = typeof tuple; // type tupleType = readonly ["tesla", "model 3", "model X", "model Y"]
type tuple2Type = typeof tuple2; // type tuple2Type = string[]
let arr: tupleType = ["tesla", 2, 3, 4];// error: Type 'number' is not assignable to type '"model 3"'.(2322)
let arr1: tupleType = ["tesla", "model 3", "model X", "model Y"];
let arr2: tuple2Type = ['str'];
我们定义了tuple
和tuple2
两个变量,其中tuple
加了 as const
修饰符,使用typeof
修饰符可以看到二者的不同:tuple
的类型为readonly ["tesla", "model 3", "model X", "model Y"]
,tuple2
的类型为string[]
。所以使用tupleType
时,必须为准确的字面量,而使用tuple2Type
只要是字符串数组就可以。
T[number]
T[number]
用于获取数组/元组类型 T
的所有元素类型,返回一个联合类型。
具体使用可以看下面代码:
ts
// 1. 基本数组类型
type Arr1 = number[];
type Elements1 = Arr1[number]; // number
// 2. 混合类型数组
type Arr2 = (string | number)[];
type Elements2 = Arr2[number]; // string | number
// 3. 元组类型
type Tuple1 = [string, number, boolean];
type Elements3 = Tuple1[number]; // string | number | boolean
// 4. 字面量元组
type Tuple2 = ["a", "b", "c"];
type Elements4 = Tuple2[number]; // "a" | "b" | "c"
// 5. 混合字面量元组
type Tuple3 = [1, "hello", true];
type Elements5 = Tuple3[number]; // 1 | "hello" | true
// 6. 空数组
type EmptyArr = [];
type Elements6 = EmptyArr[number]; // never
// 7. 只读数组
type ReadonlyArr = readonly [1, 2, 3];
type Elements7 = ReadonlyArr[number]; // 1 | 2 | 3