原文链接: Advanced TypeScript - 原文作者: kreuzercode
本文采用的是意译的方式
本文旨在熟悉些 TypeScript
的高级特性。
TypeScript
很优秀。它提供了很多很棒的特性。下面是一些不错的高级特性汇总。
- 联合类型和交叉类型(
Union and intersection types
) - Keyof
- Typeof
- 条件类型(
Conditional types
) - 实用工具类型(
Utility types
) - 推断类型(
Infer type
) - 映射类型(
Mapped types
)
联合类型和交叉类型
Typescript
允许我们结合多个类型来创建一个新的类型。这方法很像 JavaScript
中的逻辑表达式 OR ||
或者 AND &&
,我们使用它创建一个功能更强大的检查条件。
联合类型
联合类型很像 JavaScript
中的 OR
表达式。它允许我们使用两个或者更多类型(联合成员)去生成一个新的类型。
typescript
function orderProduct(orderId: string | number) {
console.log('Ordering product with id ', orderId);
}
// 👍
orderProduct(1);
// 👍
orderProduct('123-abc');
// 👎 Argument is not assignable to string | number
orderProduct({ name: 'foo' });
我们使用联合类型为 orderProduct
方法添加类型。当我们以不是 number
或者 string
类型的参数调用 orderProduct
方法,则会抛出错误。
交叉类型
交叉类型,也就是说,整合多个类型为一个类型。这个新的类型拥有所有其他类型的特性。
typescript
interface Person {
name: string,
firstname: string
}
interface FootballPlayer {
club: string;
}
function tranferPlayer(player: Person & FootballPlayer) {}
// 👍
transferPlayer({
name: 'Ramos',
firstname: 'Sergio',
club: 'PSG',
});
// 👎 Argument is not assignable to Person & FootballPlayer
transferPlayer({
name: 'Ramos',
firstname: 'Sergio'
});
transferPlayer
方法接受 Person
和 FootballPlayer
联合起来的类型。只有方法参数对象中都包含了 name
,firstname
和 club
属性,参数对象才有效。
Keyof
现在,我们知道了联合类型。我们来看看 keyof
操作符。keyof
操作符会获取 interface
或者对象 object
的键,并产生一个联合类型。
typescript
interface MovieCharacter {
firstname: string,
name: string,
movie: string
}
type characterProps = keyof MovieCharacter;
明白了!但是什么时候是有用处的呢?我们也可以直接给 characterProps
添加类型。
typescript
type characterProps = 'firstname' | 'name' | 'movie';
是的,我们可以这样做。keyof
让我们的代码更加健壮,并且始终保持我们的类型更新。我们来举个例子。
typescript
interface PizzaMenu {
starter: string,
pizza: string,
beverage: string,
dessert: string
}
const simpleMenu: PizzaMenu = {
starter: 'Salad',
pizza: 'Pepperoni',
beverage: 'Coke',
dessert: 'Vanilla ice cream',
};
function adjustMenu(
menu: PizzaMenu,
menuEntry: keyof PizzaMenu,
change: string
) {
menu[menuEntry] = change;
}
// 👍
adjustMenu(simpleMenu, 'pizza', 'Hawaii');
// 👍
adjustMenu(simpleMenu, 'beverage', 'Beer');
// 👎 Type - 'beverager' is not assignable
adjustMenu(simpleMenu, 'beverager', 'Beer');
// 👎 Wrong property - 'coffee' is not assignable
adjustMenu(simpleMenu, 'coffee', 'Beer');
函数 adjustMenu
允许你更改菜单 menu
。比如,想象下你很喜欢菜单 menuSimple
,但是你更喜欢喝啤酒 beer
而不是 Coke
。在这个例子中,我们通过参数 menu
,menuEntry
和 change
来调用 adjustMenu
函数,如上。
这个函数很有趣的一部分是,menuEntry
是使用 keyof
运算符进行类型标注。这样,我们的代码就变得很健壮。如果我们重构 PizzaMenu
接口,我们就不需要改动 adjustMenu
函数。其会通过 PizzaMenu
的键进行更新。
Typeof
typeof
允许你从一个值中提取类型。它可以在类型上下文中使用,用来引用一个变量的类型。
typescript
let firstname = 'Frodo';
let name: typeof firstname;
当然,在这个场景中并没什么用处。但是,我们看看更加复杂的例子。在这个例子中,我们联合 ReturnType
使用 typeof
来提取函数返回的类型。
typescript
function getCharacter() {
return {
firname: 'Frodo',
name: 'Baggins'
};
}
type Character = ReturnType<typeof getCharacter>;
/*
equal to
type Character = {
firstname: string;
name: string;
}
*/
在上面的例子中,我们基于 getCharacter
函数返回的类型,创建了一个新的类型。一样的,这里,我们更改了这个函数返回的类型,Character
类型随之更新。
条件类型
在 JavaScript
中,三元条件操作符我们很清楚。
javascript
condition ? returnTypeIfTrue : returnTypeIfFalse;
在 Typescript
中,该概念也存在。
typescript
interface StringId {
id: string;
}
interface NumberId {
id: number;
}
type Id<T> = T extends string ? StringId : NumberId;
let idOne: Id<string>;
// equal to let idOne: StringId;
let idTwo: Id<number>;
// equal to let idTwo: NumberId;
在这个例子中,我们基于 string
使用 Id
类型方法生成一个类型。如果 T
继承自 string
,我们返回 StringId
类型。如果我们传递了一个 number
,我们返回 NumberId
类型。
实用工具类型
实用工具类型是辅助工具,用于简化常用的类型转换。Typescript
提供了很多使用类型。本文中我们介绍了些。
在下面的链接中你可以找到更多 utility-types。
Partial(部分/可选)
Partial
实用类型允许你将一个接口转换成另外一个接口,转换后的 interface
的属性都变成可选的。
typescript
interface MovieCharacter {
firstname: string;
name: string;
movie: string;
}
function registerCharacter(character: Partial<MovieCharacter>) {}
// 👍
registerCharacter({
firstname: 'Frodo',
});
// 👍
registerCharacter({
firstname: 'Frodo',
name: 'Baggins',
});
MovieCharacter
必传 firstname
,name
和 movie
。但是,函数 registerCharacter
参数通过 Partial
实用方法转换为可选的 firstname
,可选的 name
和可选的 movie
字段。
Required(必须)
Required
与 Partial
相反。它将所有的可选属性的类型转换成必填属性的类型。
typescript
interface MovieCharacter {
firstname?: string;
name?: string;
movie?: string;
}
function hireActor(character: Required<MovieCharacter>) {}
// 👍
hireActor({
firstname: 'Frodo',
name: 'Baggins',
movie: 'The Lord of the Rings',
});
// 👎
hireActor({
firstname: 'Frodo',
name: 'Baggins',
});
Extract(提取/包含)
Extract
允许我们从一个类型中提取出你想要的信息。Extract
接收两个参数,第一个是 interface
,第二个是要提取的类型。
typescript
type MovieCharacters =
| 'Harry Potter'
| 'Tom Riddle'
| { firstname: string; name: string };
type hpCharacters = Extract<MovieCharacters, string>;
// equal to type hpCharacters = 'Harry Potter' | 'Tom Riddle';
type hpCharacters = Extract<MovieCharacters, { firstname: string }>;
// equal to type hpCharacters = { firstname: string; name: string };
Extract<MovieCharacters, string>
创建了联合类型 hpCharacters
,它由字符串构成。另一个 Extract<MovieCharacters, {firstname: string}>
提取了所有包含 firstname: string
类型。
Exclude(排除)
Exclude
和 Extract
相反。它允许我们通过排除类型来生成一个新的类型。
typescript
type MovieCharacters =
| 'Harry Potter'
| 'Tom Riddle'
| { firstname: string; name: string };
type hpCharacters = Exclude<MovieCharacters, string>;
// equal to type hpCharacters = {firstname: string; name: string };
type hpCharacters = Exclude<MovieCharacters, { firstname: string }>;
// equal to type hpCharacters = 'Harry Potter' | 'Tom Riddle';
首先,我们通过排除 string
来生成一个新的类型。然后,我们又通过排除所有对象中包含 firstname: string
来生成一个新的类型。
Infer type(推断类型)
infer
允许我们创建一个新类型。类比 Javascript
中的 var
,let
或者 const
。
typescript
type flattenArrayType<T> = T extends Array<infer ArrayType> ? ArrayType : T;
type foo = flattenArrayType<string[]>;
// equal to type foo = string;
type foo = flattenArrayType<number[]>;
// equal to type foo = number;
type foo = flattenArrayType<number>;
// equal to type foo = number;
哇,flattenArrayType
看起来很复杂。事实上并不是。我们一步步解析。
T extends Array<infer ArrayType>
检查 T
继承自一个数组。然后我们使用 infer
关键字获取到当前的数组类型。然后将其该类型存放在一个变量中。
然后,我们使用条件类型进行判断。如果 T
继承自数组,则返回数组类型,如果不是,则返回 T
。
Mapped types(映射类型)
映射类型是将已有的类型转换成新类型的一种很好的方式。因此有了 映射
这个术语。映射类型具有强大的功能,允许我们常见自定义实用工具类型。
typescript
interface Character {
playInFantasyMovie: () => void;
playInActionMovie: () => void;
}
type toFlags<Type> = { [Property in keyof Type]: boolean };
type characterFeature = toFlags<Character>;
/*
equal to
type characterFeatures = {
playInFantasyMovie: boolean;
playInActionMovie: boolean;
}
*/
我们创建了 toFlag
辅助类型,该类型接受一个类型,并将所有的属性映射为返回类型为布尔值。
很不错。但是我们可以获取更强大功能。我们可以添加或者移除 ?
或者通过简单的前缀 +
或者 -
添加 readonly
修饰词。
看下面例子,我们创建了一个 mutable
实用类型。
typescript
type mutable<Type> = {
-readonly [Property in keyof Type]: Type[Property];
};
type Character = {
readonly firstname: string;
readonly name: string;
};
type mutableCharacter = mutable<Character>;
/*
equal to
type mutableCharacter = {
firstname: string;
name: string;
}
*/
Character
类型中的每个属性都是只读的。我们的 mutable
接口移除了 readonly
属性,因为我们使用了 -
前缀。
同理。如果我们添加了 +
,我们可以创建辅助类型,可以将接口转换成可选的属性,如下。
typescript
type optional<Type> = {
[Property in keyof Type]+?: Type[Property];
};
type Character = {
firstname: string;
name: string;
};
type mutableCharacter = optional<Character>;
/*
equal to
type mutableCharacter = {
firstname?: string;
name?: string;
}
*/
当然,这两种方法可以合并。我们看看下面这个 optionalAndMutable
类型,其移除 readonly
属性,且添加 ?
让每个属性可选。
typescript
type optionalAndMutable<Type> = {
-readonly [Property in keyof Type]+?: Type[Property];
};
type Character = {
readonly firstname: string;
readonly name: string;
};
type mutableCharacter = optionalAndMutable<Character>;
/*
equal to
type mutableCharacter = {
firstname?: string;
name?: string;
}
*/
它甚至可变得更强大了。如下例子,我们将存在的类型转换成 setters
类型。
typescript
type setters<Type> = {
[Property in keyof Type as `set${Capitalize<
string & Property
>}`]: () => Type[Property];
};
type Character = {
firstname: string;
name: string;
};
type character = setters<Character>;
/*
equal to
type character = {
setFirstname: () => string;
setName: () => string;
}
*/
这里没有任何限制。我们甚至可以重用我们目前看到的知识点。结合 Exclude
实用类型如何?
typescript
type nameOnly<Type> = {
[Property in keyof Type as Exclude<Property, 'firstname'>]: Type[Property];
};
type Character = {
firstname: string;
name: string;
};
type character = nameOnly<Character>;
/*
equal to
type character = {
name: string;
}
*/
Typescript
很棒,它提供了更多的特性。一旦我们掌握了本文描述的概念,我们可以让自己写的代码更加健壮并容易重构。