类型操作
复合类型
TypeScript的复合类型可以分为两类:set和map。set是指一个无序的、无重复元素的集合。而map则和JS中的对象一样,是一些没有重复键的键值对。
// set
type Size = 'small' | 'default' | 'big' | 'large';
// map
interface IA {
a: string
b: number
}
复合类型键的转换
// map => set
type IAKeys = keyof IA; // 'a' | 'b'
type IAValues = IA[keyof IA]; // string | number
// set => map
type SizeMap = {
[k in Size]: number
}
// 等价于
type SizeMap2 = {
small: number
default: number
big: number
large: number
}
map上的操作
// 索引取值
type SubA = IA['a']; // string
// 属性修饰符
type Person = {
age: number
readonly name: string // 只读属性,初始化时必须赋值
nickname?: string
}
映射类型和同态变换
在TypeScript中,有以下几种常见的映射类型。它们的共同点是只接受一个传入类型,生成的类型中key 都来自于keyof传入的类型,value都是传入类型的value的变种。
type Partial<T> = { [P in keyof T]?: T[P] } // 将一个map所有属性变为可选的
type Required<T> = { [P in keyof T]-?: T[P] } // 将一个map所有属性变为必选的
type Readonly<T> = { readonly [P in keyof T]: T[P] } // 将一个map所有属性变为只读的
type Mutable<T> = { -readonly [P in keyof T]: T[P] } // ts标准库未包含,将一个map所有属性变为可写的
此类变换,在TS中被称为同态变换。在进行同态变换时,TS会先复制一遍传入参数的属性修饰符,再应用定义的变换。
interface Fruit {
readonly name: string
size: number
}
type PF = Partial<Fruit>; // PF.name既只读又可选,PF.size只可选
其他常用工具类型
由set生成map
type Record<K extends keyof any, T> = { [P in K]: T };
type Size = 'small' | 'default' | 'big';
/*
{
small: number
default: number
big: number
}
*/
type SizeMap = Record<Size, number>
保留map的一部分
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
/*
{
default: number
big: number
}
*/
type BiggerSizeMap = Pick<SizeMap, 'default' | 'big'>;
删除map的一部分
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
/*
{
default: number
}
*/
type DefaultSizeMap = Omit<BiggerSizeMap, 'big'>;
保留set的一部分
type Extract<T, U> = T extends U ? T : never;
type Result = 1 | 2 | 3 | 'error' | 'success';
type StringResult = Extract<Result, string>; // 'error' | 'success'
删除set的一部分
type Exclude<T, U> = T extends U ? never : T;
type NumericResult = Exclude<Result, string>; // 1 | 2 | 3
获取函数返回值的类型。但要注意不要滥用这个工具类型,应该尽量多手动标注函数返回值类型,契约高于实现。用ReturnType 是由实现反推契约,而实现往往容易变容易出错,契约则相对稳定。另一方 面,ReturnType 过多也会降低代码可读性。
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
function f() { return { a: 3, b: 2} }
/*
{
a: number
b: number
}
*/
type FReturn = ReturnType<typeof f>;
以上这些工具类型都已经包含在了TS标准库中,在应用中直接输入名字进行使用即可。另外,在这些工 具类型的实现中,出现了infer、never、typeof等关键字。
类型的递归
TS原生的Readonly只会限制一层写入操作,我们可以利用递归来实现深层次的Readonly。但要注意, TS对最大递归层数做了限制,最多递归5层。
type DeepReadonly<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>
}
interface SomeObject {
a: {
b: {
c: number;
}
}
}
const obj: Readonly<SomeObject> = {a: {b: { c: 2}}};
obj.a.b.c = 3; // TS不会报错
const obj2: DeepReadonly<SomeObject> = {a: {b: {c: 2}}};
obj2.a.b.c = 3; // Cannot assign to 'C' because it is a read-only property.
never infer typeof关键字
never 是 | 运算的幺元,即 x | never = x。例如之前的Exclude<Result, string>
运算过程如下:
1 | 2 | 3 | 'error' | 'success'
Exclude string
1 | 2 | 3 | never | never
1 | 2 | 3
infer的作用是让TypeScript自己推断,并将推断的结果存储到一个临时名字中,并且只能用于extends 语句中。它与泛型的区别在于,泛型是声明一个“参数”,而infer是声明一个“中间变量”。infer用的比较少,官方示例如下:
type Unpacked<T> =
T extends (infer U)[] ? U :
T extends (...args: any[]) => infer U ? U :
T extends Promise<infer U> ? U :
T;
type T0 = Unpacked<string>; // string
type T1 = Unpacked<string[]>; // string
type T2 = Unpacked<() => string>; // string
type T3 = Unpacked<Promise<string>>; // string
type T4 = Unpacked<Promise<string>[]>; // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>; // string
typeof用于获取一个“常量”的类型,这里的“常量”是指任何可以在编译期确定的东西,例如const、 function、class等。它是从实际运行代码通向类型系统的单行道。理论上,任何运行时的符号名想要为 类型系统所用,都要加上 typeof。但是class比较特殊不需要加,因为ts的class出现得比js早,现有的为 兼容性解决方案。
在使用class时,class名表示实例类型,typeof class 表示class本身类型。没错,这个关键字和js的 typeof关键字重名了。
const config = { width: 2, height: 2 };
function getLength(str: string) { return str.length }
type TConfig = typeof config; // { width: number, height: number }
type TGetLength = typeof getLength; // (str: string) => number
实战
在项目中遇到这样一种场景,需要获取一个类型中所有value为指定类型的key。例如,已知某个React 组件的props类型,我们需要“知道”(编程意义上)哪些参数是function类型。
interface SomeProps {
a: string
b: number
c: (e: MouseEvent) => void
d: (e: TouchEvent) => void
}
// 如何得到 ’c' | 'd' ?
分析一下这里的思路,我们需要从一个map得到一个 set,而这个 set 是 map 的key 的子集,筛选子集的条件是value的类型。要构造set的子集,需要用到 never;要实现条件判断,需要用到 extends;而要实现key到value的访问,则需要索引取值。经过一些尝试后,解决方案如下:
type GetKeyByValueType<T, Condition> = {
[K in keyof T]: T[K] extends Condition ? K : never
} [keyof T];
type FunctionPropNames = GetKeyByValueType<SomeProps, Function>;
这里的运算过程如下:
// 开始
{
a: string
b: number
c: (e: MouseEvent) => void
d: (e: TouchEvent) => void
}
// 第一步,条件映射
{
a: never
b: never
c: 'c'
d: 'd'
}
// 第二步,索引取值
never | never | 'c' | 'd'
// never的性质
'c' | 'd'