TypeScript(七)类型操作

338 阅读5分钟

类型操作

复合类型

TypeScript的复合类型可以分为两类:set和map。set是指一个无序的、无重复元素的集合。而map则和JS中的对象一样,是一些没有重复键的键值对。

// set
type Size = 'small' | 'default' | 'big' | 'large';
// map
interface IA {
    astring
    bnumber
}

复合类型键的转换

// 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 = {
    smallnumber
    defaultnumber
    bignumber
    largenumber
}

map上的操作

// 索引取值
type SubA = IA['a']; // string// 属性修饰符
type Person = {
    agenumber
    readonly namestring // 只读属性,初始化时必须赋值
    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 namestring
    sizenumber
}
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<Sizenumber>

保留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<Resultstring>; // 'error' | 'success'

删除set的一部分

type Exclude<T, U> = T extends U ? never : T;
type NumericResult = Exclude<Resultstring>; // 1 | 2 | 3

获取函数返回值的类型。但要注意不要滥用这个工具类型,应该尽量多手动标注函数返回值类型,契约高于实现。用ReturnType 是由实现反推契约,而实现往往容易变容易出错,契约则相对稳定。另一方 面,ReturnType 过多也会降低代码可读性。

type ReturnType<T> = T extends (...argsany[]) => infer R ? R : any;
​
function f() { return { a3b2} }
/*
{
    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: {
        cnumber;
        }
    }
}
​
const objReadonly<SomeObject> = {a: {b: { c2}}};
obj.a.b.c = 3// TS不会报错const obj2DeepReadonly<SomeObject> = {a: {b: {c2}}};
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 (...argsany[]) => 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 = { width2height2 };
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 {
    astring
    bnumber
    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<SomePropsFunction>;

这里的运算过程如下:

// 开始
{
    astring
    bnumber
    c: (e: MouseEvent) => void
    d: (e: TouchEvent) => void
}
// 第一步,条件映射
{
    anever
    bnever
    c'c'
    d'd'
}
// 第二步,索引取值
never | never | 'c' | 'd'
// never的性质
'c' | 'd'