16. TypeScript 类型运算符

TS系列356933 阅读0

TypeScript 提供强大的类型运算能力,可以使用各种类型运算符,对已有的类型进行计算,得到新类型。

keyof

注意区分typeof,typeof是用于查看一个变量的类型

keyof 是一个单目运算符,接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型。


type MyObj = {
foo: number,
bar: string,
};

type Keys = keyof MyObj; // 'foo'|'bar'

由于 JavaScript 对象的键名只有三种类型,所以对于任意对象的键名的联合类型就是string|number|symbol。

// string | number | symbol
type KeyT = keyof any;

keyof 运算符往往用于精确表达对象的属性类型。

举例来说,取出对象的某个指定属性的值,JavaScript 版本可以写成下面这样。


function prop(obj, key) {
return obj[key];
}

上面这个函数添加类型,只能写成下面这样。


function prop(
obj: { [p:string]: any },
key: string
):any {
return obj[key];
}

上面的类型声明有两个问题,一是无法表示参数key与参数obj之间的关系,二是返回值类型只能写成any。

有了 keyof 以后,就可以解决这两个问题,精确表达返回值类型。


function prop<Obj, K extends keyof Obj>(
obj:Obj, key:K
):Obj[K] {
return obj[key];
}

上面示例中,K extends keyof Obj表示K是Obj的一个属性名,传入其他字符串会报错。返回值类型Obj[K]就表示K这个属性值的类型。

in 运算符

JavaScript 语言中,in运算符用来确定对象是否包含某个属性名。

const obj = { a: 123 };

if ('a' in obj)
console.log('found a');

上面示例中,in运算符用来判断对象obj是否包含属性a。

in运算符的左侧是一个字符串,表示属性名,右侧是一个对象。它的返回值是一个布尔值。

TypeScript 语言的类型运算中,in运算符有不同的用法,用来取出(遍历)联合类型的每一个成员类型。


type U = 'a'|'b'|'c';

type Foo = {
[Prop in U]: number;
};
// 等同于
type Foo = {
a: number,
b: number,
c: number
};

上面示例中,[Prop in U]表示依次取出联合类型U的每一个成员。

方括号运算符

方括号运算符([])用于取出对象的键值类型,比如T[K]会返回对象T的属性K的类型。


type Person = {
age: number;
name: string;
alive: boolean;
};

// Age 的类型是 number
type Age = Person['age'];

上面示例中,Person['age']返回属性age的类型,本例是number。

方括号的参数如果是联合类型,那么返回的也是联合类型。


type Person = {
age: number;
name: string;
alive: boolean;
};

// number|string
type T = Person['age'|'name'];

// number|string|boolean
type A = Person[keyof Person];

extends...?: 条件运算符

TypeScript 提供类似 JavaScript 的?:运算符这样的三元运算符,但多出了一个extends关键字。

条件运算符extends...?:可以根据当前类型是否符合某种条件,返回不同的类型。

T extends U ? X : Y

上面式子中的extends用来判断,类型T是否可以赋值给类型U,即T是否为U的子类型,这里的T和U可以是任意类型。

如果T能够赋值给类型U,表达式的结果为类型X,否则结果为类型Y。

// true
type T = 1 extends number ? true : false;

上面示例中,1是number的子类型,所以返回true。

下面是另外一个例子。


interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}

// number
type T1 = Dog extends Animal ? number : string;

// string
type T2 = RegExp extends Animal ? number : string;

上面示例中,Dog是Animal的子类型,所以T1的类型是number。RegExp不是Animal的子类型,所以T2的类型是string。

一般来说,调换extends两侧类型,会返回相反的结果。举例来说,有两个类Cat和Animal,前者是后者的子类型,那么Cat extends Animal就为真,而Animal extends Cat就为伪。

infer 关键字

infer关键字用来定义泛型里面推断出来的类型参数,而不是外部传入的类型参数。

它通常跟条件运算符一起使用,用在extends关键字后面的父类型之中。


type Flatten<Type> =
Type extends Array<infer Item> ? Item : Type;

上面示例中,infer Item表示Item这个参数是 TypeScript 自己推断出来的,不用显式传入,而Flatten则表示Type这个类型参数是外部传入的。Type extends Array则表示,如果参数Type是一个数组,那么就将该数组的成员类型推断为Item,即Item是从Type推断出来的。

一旦使用Infer Item定义了Item,后面的代码就可以直接调用Item了。下面是上例的泛型Flatten的用法。

// string
type Str = Flatten<string[]>;

// number
type Num = Flatten<number>;

上面示例中,第一个例子Flatten<string[]>传入的类型参数是string[],可以推断出Item的类型是string,所以返回的是string。第二个例子Flatten传入的类型参数是number,它不是数组,所以直接返回自身。

如果不用infer定义类型参数,那么就要传入两个类型参数。

type Flatten<Type, Item> =
Type extends Array<Item> ? Item : Type;

上面是不使用infer的写法,每次调用Flatten的时候,都要传入两个参数,就比较麻烦。

is 运算符

函数返回布尔值的时候,可以使用is运算符,限定返回值与参数之间的关系。

is运算符用来描述返回值属于true还是false。

is运算符总是用于描述函数的返回值类型,写法采用parameterName is Type的形式,即左侧为当前函数的参数名,右侧为某一种类型。它返回一个布尔值,表示左侧参数是否属于右侧的类型。


type A = { a: string };
type B = { b: string };

function isTypeA(x: A|B): x is A {
if ('a' in x) return true;
return false;
}

is运算符可以用于类型保护。


function isCat(a:any): a is Cat {
return a.name === 'kitty';
}

let x:Cat|Dog;

if (isCat(x)) {
x.meow(); // 正确,因为 x 肯定是 Cat 类型
}

上面示例中,函数isCat()的返回类型是a is Cat,它是一个布尔值。后面的if语句就用这个返回值进行判断,从而起到类型保护的作用,确保x是 Cat 类型,从而x.meow()不会报错(假定Cat类型拥有meow()方法)。

模板字符串

TypeScript 允许使用模板字符串,构建类型。

模板字符串的最大特点,就是内部可以引用其他类型

type World = "world";

// "hello world"
type Greeting = `hello ${World}`;

上面示例中,类型Greeting是一个模板字符串,里面引用了另一个字符串类型world,因此Greeting实际上是字符串hello world。

注意,模板字符串可以引用的类型一共7种,分别是 string、number、bigint、boolean、null、undefined、Enum。引用这7种以外的类型会报错。

type Num = 123;
type Obj = { n : 123 };

type T1 = `${Num} received`; // 正确
type T2 = `${Obj} received`; // 报错

上面示例中,模板字符串引用数值类型的别名Num是可以的,但是引用对象类型的别名Obj就会报错。

模板字符串里面引用的类型,如果是一个联合类型,那么它返回的也是一个联合类型,即模板字符串可以展开联合类型。

type T = 'A'|'B';

// "A_id"|"B_id"
type U = `${T}_id`;

上面示例中,类型U是一个模板字符串,里面引用了一个联合类型T,导致最后得到的也是一个联合类型。

如果模板字符串引用两个联合类型,它会交叉展开这两个类型。

type T = 'A'|'B';

type U = '1'|'2';

// 'A1'|'A2'|'B1'|'B2'
type V = `${T}${U}`;

上面示例中,T和U都是联合类型,各自有两个成员,模板字符串里面引用了这两个类型,最后得到的就是一个4个成员的联合类型。

satisfies 运算符

satisfies运算符用来检测某个值是否符合指定类型。有时候,不方便将某个值指定为某种类型,但是希望这个值符合类型条件,这时候就可以用satisfies运算符对其进行检测。TypeScript 4.9添加了这个运算符。

举例来说,有一个对象的属性名拼写错误。

const palette = {
red: [255, 0, 0],
green: "#00ff00",
bleu: [0, 0, 255] // 属性名拼写错误
};

上面示例中,对象palette的属性名拼写错了,将blue拼成了bleu,我们希望通过指定类型,发现这个错误。

type Colors = "red" | "green" | "blue";
type RGB = [number, number, number];

const palette: Record<Colors, string|RGB> = {
red: [255, 0, 0],
green: "#00ff00",
bleu: [0, 0, 255] // 报错
};

上面示例中,变量palette的类型被指定为Record<Colors, string|RGB>,这是一个类型工具,用来返回一个对象。简单说,它的第一个类型参数指定对象的属性名,第二个类型参数指定对象的属性值。

本例的Record<Colors, string|RGB>,就表示变量palette的属性名应该符合类型Colors,属性值应该符合类型string|RGB,要么是字符串,要么是元组RGB。属性名bleu不符合类型Colors,所以就报错了。

这样的写法,虽然可以发现属性名的拼写错误,但是带来了新的问题。

const greenComponent = palette.green.substring(1, 6); // 报错

上面示例中,palette.green属性调用substring()方法会报错,原因是这个方法只有字符串才有,而palette.green的类型是string|RGB,除了字符串,还可能是元组RGB,而元组并不存在substring()方法,所以报错了。

如果要避免报错,要么精确给出变量palette每个属性的类型,要么对palette.green的值进行类型缩小。两种做法都比较麻烦,也不是很有必要。

这时就可以使用satisfies运算符,对palette进行类型检测,但是不改变 TypeScript 对palette的类型推断。


type Colors = "red" | "green" | "blue";
type RGB = [number, number, number];

const palette = {
red: [255, 0, 0],
green: "#00ff00",
bleu: [0, 0, 255] // 报错
} satisfies Record<Colors, string|RGB>;

const greenComponent = palette.green.substring(1); // 不报错

上面示例中,变量palette的值后面增加了satisfies Record<Colors, string|RGB>,表示该值必须满足Record<Colors, string|RGB>这个条件,所以能够检测出属性名bleu的拼写错误。同时,它不会改变palette的类型推断,所以,TypeScript 知道palette.green是一个字符串,对其调用substring()方法就不会报错。

satisfies也可以检测属性值。

const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0] // 报错
} satisfies Record<Colors, string|RGB>;

上面示例中,属性blue的值只有两个成员,不符合元组RGB必须有三个成员的条件,从而报错了。

评论

发表评论