我们先看看平常使用的ts工具的实现
type Partial<T> = {
[P in keyof T]?: T[P];
};
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
从上面是不是能看出一些端倪,比如[],in,keyof,extends,infer等等,本文再复习一下这些操作符的用法
索引类型
使用索引类型,编译器就能够检查使用了动态属性名的代码
索引类型查询操作符,keyof T
interface Car {
manufacturer: string;
model: string;
year: number;
}
let carProps: keyof Car // 'manufacturer' | 'model' | 'year'
从上面的例子可以看出,keyof是集合了每个属性key的一个联合类型。事实上keyof Car 是完全可以和'manufacturer' | 'model' | 'year'相互替换的,但是使用keyof我们就可以动态获取类型Car的所有属性,比如Car可能会新增属性ownersAddress: string等等
当然这里还会有一些奇怪的地方,比如如果我们对基本类型使用keyof呢?比如keyof '',keyof 1,基本上我们不会这么使用,实际上这里使用了基本类型的对应构造函数的原型属性
let carProps: keyof ''
// let carProps: number | typeof Symbol.iterator | "toString" | "charAt" | "charCodeAt" | "concat" | "indexOf" | "lastIndexOf" | "localeCompare" | "match" | "replace" | "search" | "slice" | ... 35 more ... | "matchAll"
let carProps: keyof 1
// let carProps: "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"
索引访问操作符,T[K]
和我们在js中使用表达式语法a[b]很像,这里可以简单把操作的类型想象成一个对象
interface Car {
manufacturer: string;
model: string;
year: number;
}
let carProps: Car['manufacturer']
我们可以直接在普通上下文中直接使用T[K],只要保证K extends keyof T就好了,我们可以再看一个稍微复杂的例子
interface Car {
manufacturer: string;
model: string;
year: number;
}
let taxi: Car = {
manufacturer: 'Toyota',
model: 'Camry',
year: 2014
}
function getProperty<T, K extends keyof T>(o: T, propertyName: K): T[K] {
return o[propertyName]; // o[propertyName] is of type T[K]
}
let carProps: string = getProperty(taxi, 'model')
let year: number = getProperty(taxi, 'year');
字符串索引签名
在学习接口的时候我们见过这样的语法。我们回顾一下知识点
索引签名的参数类型必须为 number 或 string,数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为应当使用 number 来索引时,JavaScript 会将它转换成 string 然后再去索引对象。 也就是说用 100 (一个 number)去索引等同于使用 "100" (一个 string )去索引,因此两者需要保持一致。
interface Dictionary {
[key: string]: string
}
let dictionary: Dictionary = {
a: '',
1: ''
}
type Dict = keyof Dictionary // string | number
映射类型 in操作符
常见的使用场景是将一个已知类型的每个属性都变成只读的,下面是Readonly工具的实现
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
来看看最简单的实现
type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };
我们可以发现in和索引签名类型一样,内部都使用了for...in,并且完成了以下三步操作
- 获取一个联合类型的集合,包含了要迭代的所有属性名,这里采用的硬编码形式
- 类型变量K依次绑定到每个属性
- 约束属性的结果类型
实际就等同于
type Flags = {
option1: boolean;
option2: boolean;
}
真正的使用场景中不会干巴巴的使用联合类型,而基于已存在的类型,按照一定的方式进行转换。也就是我们上面看到的Readonly实现方式。in操作符搭配keyof和索引访问类型一起操作
条件类型 extends操作符
语法如下
T extends U ? X : Y
若 T 能够赋值给 U ,那么类型是 X ,否则为 Y
在实际使用中用到的大多都是条件类型的嵌套方式,比如下面一个简单的使用
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
分布式有条件类型
如果有条件类型里待检查的类型是裸类型,那么它也被称为 “分布式有条件类型”。
什么是裸类型呢?
是指类型参数没有被包装在其他类型里,比如没有被数组、元组、函数、Promise等等包裹,简而言之裸类型就是未经过任何其他类型修饰或包装的类型。
type TypeName<T> = T extends number ? "X" : "Y" ;
type H = TypeName<string | number> // "X" | "Y"
如果我们给类型包裹一下呢
type TypeName<T> = [T] extends [number] ? "X" : "Y" ;
type H = TypeName<string | number> // "Y"
从上面的例子我们能看出是否裸类型的区别,那么裸类型的返回值又应该怎么理解呢
分布式有条件类型在实例化的时候会自动分发成联合类型,例如实例化 T extends U ? X : Y , T 的类型为 A | B | C ,会被解析为 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y) 。
我们看看一个工具类型的实现
type Exclude<T, U> = T extends U ? never : T;
type ExcludeTest = Exclude<'a' | 'b' | 'c' | 'd', 'a' | 'b'> // 'c' | 'd'
每次都拿联合类型'a' | 'b' | 'c' | 'd'中的某一个类型去和'a' | 'b'判断,是否是其子类型
条件类型中的类型推断 infer操作符
先看看ts工具ReturnType
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
type ReturnTypeTest = ReturnType<() => string> // string
现在在有条件类型的 extends 子语句中,允许出现 infer 声明,它会引入一个待推断的类型变量。 这个推断的类型变量可以在有条件类型的 true 分支中被引用。 允许出现多个同类型变量的 infer 。
使用infer的时候注意一下协变逆变的区别
当infer出现在协变位置上时,同一个类型变量的多个候选类型会被推断成联合类型
type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>; // string
type T11 = Foo<{ a: string, b: number }>; // string | number
当infer出现在协变位置上时,同一个类型变量的多个候选类型会被推断成交叉类型
type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>; // string
type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>; // string & number => never
模板字面量类型
模板字面类型建立在 字符串字面类型
之上,并且能够通过联合扩展成许多字符串。当模板字面量中有多个插值位置时,联合是交叉相乘
的
type World = "world";
// "hello world"
type Greeting = `hello ${World}`;
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
// "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
模板字面进行推理
这个就用的很多了,我们先看一个TS challenge中的一个例子
type Space = ' ' | '\n' | '\t'
// 递归一层层实现
type TrimLeft<S extends string> = S extends `${Space}${infer R}` ? TrimLeft<R> : S
type Test = TrimLeft<' str'>
let a: Test = 'str'
使用infer,我们能推断出剩余字符构成的新字符串字面量类型,结合递归和字符串的工具类型能完成很多意想不到的操作
字符串的工具类型有
- Uppercase 将字符串中的每个字符转换为大写版本
- Lowercase 将字符串中的每个字符转换为等效的小写字母
- Capitalize 将字符串中的第一个字符转换为等效的大写字母
- Uncapitalize 将字符串中的第一个字符转换为等效的小写字母