从类型中创造类型
- 泛型类型
- Keyof类型操作符
- typeof类型操作符
- 索引访问类型
- 条件类型
- 映射类型
- 模板字面量类型
泛型
首先,让我们做一下泛型的 " hello world":身份函数。身份函数是一个函数,它将返回传入的任何内容。你可以用类似于echo命令的方式来考虑它。
如果没有泛型,我们将不得不给身份函数一个特定的类型。
function identity(arg: number): number {
return arg;
}
或者,我们可以用任意类型来描述身份函数。
function identity(arg: any): any {
return arg;
}
但是这些都不能实现我们的需求,我们想要的是一个能在各种类型上工作的组件,我们不应该对他有类型限制和要求。所以我们需要一种方法来捕获参数的类型:
function identity<Type>(arg: Type): Type {
return arg;
}
我们现在已经在身份函数中添加了一个类型变量 Type 。这个 Type 允许我们捕获用户提供的类型(例 如数字),这样我们就可以在以后使用这些信息。这里,我们再次使用Type作为返回类型。经过检查, 我们现在可以看到参数和返回类型使用的是相同的类型。这使得我们可以将类型信息从函数的一侧输入,然后从另一侧输出。
let output = identity<string>("myString");
也可以进行参数类型推断:
let output = identity("myString");
使用通用类型
让我们来看看我们前面的 identity 函数。
function identity<Type>(arg: Type): Type {
return arg;
}
如果我们想在每次调用时将参数 arg 的长度记录到控制台,该怎么办?我们可能很想这样写:
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
return arg;
}
欸?为什么会出现这样的问题。因为我们使用length属性时如果arg为数组的话可以访问,但是二u给arg是数字类型的时候就会出现问题。
那我们就要使用通用类型了:
比方说,我们实际上是想让这个函数在 Type 的数组上工作,而不是直接在 Type 上工作。既然我们在处理数组,那么 .length 成员应该是可用的。我们可以像创建其他类型的数组那样来描述它。
function loggingIdentity<Type>(arg: Type[]): Type[] {
console.log(arg.length);
return arg;
}
你可以把 loggingIdentity 的类型理解为 "通用函数 loggingIdentity 接收一个类型参数 Type 和 一个参数 arg , arg 是一个 Type 数组,并返回一个 Type 数组。" 如果我们传入一个数字数组,我们会 得到一个数字数组,因为Type会绑定到数字。这允许我们使用我们的通用类型变量 Type 作为我们正在处理的类型的一部分,而不是整个类型,给我们更大的灵活性。
我们也可以这样来写这个例子:
function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
console.log(arg.length); // 数组有一个.length,所以不会再出错了
return arg;
}
你可能已经从其他语言中熟悉了这种类型的风格。在下一节中,我们将介绍如何创建你自己的通用类 型,如 Array<Type> 。
泛型类型
函数本身的类型以及如何创建通用接口
泛型函数的类型与非泛型函数的类型一样,类型参数列在前面,与函数声明类似:
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: <Type>(arg: Type) => Type = identity;
//我们也可以为类型中的通用类型参数使用一个不同的名字,只要类型变量的数量和类型变量的使用方式
一致。
let myIdemtify: <Input>(arg: Typr) => Typr = indntity
我们也可以为类型中的通用类型参数使用一个不同的名字,只要类型变量的数量和类型变量的使用方式一致。
我们也可以把泛型写成一个对象字面类型的调用签名。
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: { <Type>(arg: Type): Type } = identity;
这让我们开始编写我们的第一个泛型接口。让我们把前面例子中的对象字面意思移到一个接口中。
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
在一个类似的例子中,我们可能想把通用参数移到整个接口的参数上。这可以让我们看到我们的泛型是 什么类型(例如, Dictionary 而不是仅仅 Dictionary )。这使得类型参数对接口的所有其 他成员可见。
interface GenericIdentityFn<Type> {
(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
请注意,我们的例子已经改变了,变成了稍微不同的东西。我们现在没有描述一个泛型函数,而是有一 个非泛型的函数签名,它是泛型类型的一部分。当我们使用 GenericIdentityFn 时,我们现在还需要 指定相应的类型参数(这里是:数字),有效地锁定了底层调用签名将使用什么。了解什么时候把类型 参数直接放在调用签名上,什么时候把它放在接口本身,将有助于描述一个类型的哪些方面是通用的。 除了泛型接口之外,我们还可以创建泛型类。注意,不可能创建泛型枚举和命名空间。
泛型类
一个泛型类的形状与泛型接口相似。泛型类在类的名字后面有一个角括号(<>)中的泛型参数列表。
class GenericNumber<NumType> {
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};
这是对 GenericNumber 类相当直白的使用,但你可能已经注意到,没有任何东西限制它只能使用数字类型。我们本可以使用字符串或更复杂的对象。
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
return x + y;
}
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
就像接口一样,把类型参数放在类本身,可以让我们确保类的所有属性都与相同的类型一起工作。
一个类的类型有两个方面:静态方面和实例方面。通用类只在其实例侧而非静态侧具有通用性,所以在使用类时,静态成员不能使用类的类型参数。
泛型约束
就是我们刚才最初的函数使用泛型,但是我们想要在后面访问它的length属性时,就会出现问题。
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
return arg;
}
我们希望限制这个函数与 any 和所有类型一起工作,而不是与 any 和所有同时具有 .length 属性的 类型一起工作。只要这个类型有这个成员,我们就允许它,但它必须至少有这个成员。要做到这一点, 我们必须把我们的要求作为一个约束条件列在 Type 可以是什么。
为了做到这一点,我们将创建一个接口来描述我们的约束。在这里,我们将创建一个接口,它有一个单 一的 .length 属性,然后我们将使用这个接和 extends 关键字来表示我们的约束条件。
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length); // 现在我们知道它有一个 .length 属性,所以不再有错误了
return arg;
}
因为泛型函数现在被限制了,它将不再对 any 和 所有的类型起作用。
loggingIdentity(3);
相反,我们需要传入其类型具有所有所需属性的值。
loggingIdentity({ length: 10, value: 3 });
在泛型约束中使用类型参数
你可以声明一个受另一个类型参数约束的类型参数。例如,在这里我们想从一个给定名称的对象中获取 一个属性。我们想确保我们不会意外地获取一个不存在于 obj 上的属性,所以我们要在这两种类型之间 放置一个约束条件。
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m");
在泛型中使用类类型
在TypeScript中使用泛型创建工厂时,有必要通过其构造函数来引用类的类型。比如说:
function create<Type>(c: { new (): Type }): Type {
return new c();
}
一个更高级的例子,使用原型属性来推断和约束类类型的构造函数和实例方之间的关系。
class BeeKeeper {
hasMask: boolean = true;
}
class ZooKeeper {
nametag: string = "Mikle";
}
class Animal {
numLegs: number = 4;
}
class Bee extends Animal {
keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;
Keyof类型操作符
keyof 运算符接收一个对象类型,并产生其键的字符串或数字字面联合。下面的类型P与 "x"|"y "是同一 类型。
type Point = { x: number; y: number };
type P = keyof Point;
const p1:P = 'x'
const p2:P = 'y'
如果该类型有一个字符串或数字索引签名, keyof 将返回这些类型。
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;
const a:A = 0
type Mapish = { [k: string]: boolean };
type M = keyof Mapish;
const m:M = 'a'
const m2:M = 10
注意,在这个例子中, M 是 string|number ——这是因为JavaScript对象的键总是被强制为字符串,所 以 obj[0] 总是与 obj["0"] 相同。
keyof 类型在与映射类型结合时变得特别有用,我们将在后面进一步了解。
Typeof 类型操作符
JavaScript已经有一个 typeof 操作符,你可以在表达式上下文中使用。
// 输出 "string"
console.log(typeof "Hello world");
TypeScript添加了一个 typeof 操作符,你可以在类型上下文中使用它来引用一个变量或属性的类型。
let s = "hello";
let n: typeof s;
n = 'world'
n= 100
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<f>;
这对基本类型来说不是很有用,但结合其他类型操作符,你可以使用typeof来方便地表达许多模式。举 一个例子,让我们先看看预定义的类型 ReturnType 。它接收一个函数类型并产生其返回类型:
type Predicate = (x: unknown) => boolean;
type K = ReturnType<Predicate>;
如果我们试图在一个函数名上使用 ReturnType ,我们会看到一个指示性的错误。
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<f>;
请记住,值和类型并不是一回事。为了指代值f的类型,我们使用 typeof 。
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<typeof f>;
TypeScript 故意限制了你可以使用 typeof 的表达式种类。
具体来说,只有在标识符(即变量名)或其属性上使用typeof是合法的。这有助于避免混乱的陷阱,即 编写你认为是在执行的代码,但其实不是。
// 我们认为使用 = ReturnType<typeof msgbox>
let shouldContinue: typeof msgbox("Are you sure you want to continue?");
索引访问类型
我们可以使用一个索引访问类型来查询另一个类型上的特定属性:
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"];
索引类型本身就是一个类型,所以我们可以完全使用 unions、 keyof 或者其他类型。
interface Person {
name: string
age: number
alive: boolean
}
// type I1 = string | number
type I1 = Person["age" | "name"];
const i11:I1 = 100
const i12:I1 = ''
// type I2 = string | number | boolean
type I2 = Person[keyof Person];
const i21:I2 = ''
const i22:I2 = 100
const i23:I2 = false
// type I3 = Person[AliveOrName];
type AliveOrName = "alive" | "name";
const aon1:AliveOrName = 'alive'
const aon2:AliveOrName = 'name'
如果你试图索引一个不存在的属性,你甚至会看到一个错误:
type I1 = Person["alve"];
另一个使用任意类型进行索引的例子是使用 number 来获取一个数组元素的类型。我们可以把它和 typeof 结合起来,方便地获取一个数组字面的元素类型。
const MyArray = [
{ name: "Alice", age: 15 },
{ name: "Bob", age: 23 },
{ name: "Eve", age: 38 },
];
/* type Person = {
name: string;
age: number;
} */
type Person = typeof MyArray[number];
const p:Person = {
name: 'xiaoqian',
age: 11
}
// type Age = number
type Age = typeof MyArray[number]["age"];
const age:Age = 11
// 或者
// type Age2 = number
type Age2 = Person["age"];
const age2:Age2 = 11
你只能在索引时使用类型,这意味着你不能使用 const 来做一个变量引用:
const key = "age";
type Age = Person[key];
条件类型
在大多数有用的程序的核心,我们必须根据输入来做决定。JavaScript程序也不例外,但鉴于数值可以很 容易地被内省,这些决定也是基于输入的类型。条件类型有助于描述输入和输出的类型之间的关系。
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
// type Example1 = number
type Example1 = Dog extends Animal ? number : string;
// type Example2 = string
type Example2 = RegExp extends Animal ? number : string;
条件类型的形式看起来有点像JavaScript中的条件表达式( condition ? trueExpression : falseExpression )。
SomeType extends OtherType ? TrueType : FalseType;
当 extends 左边的类型可以赋值给右边的类型时,那么你将得到第一个分支中的类型("真 "分支); 否则你将得到后一个分支中的类型("假 "分支)。
从上面的例子来看,条件类型可能并不立即显得有用——我们可以告诉自己是否 Dog extends Animal ,并选择 number 或 string !但条件类型的威力来自于它所带来的好处。条件类型的力量来自于将它们与泛型一起使用。
例如,让我们来看看下面这个 createLabel 函数:
interface IdLabel {
id: number /* 一些字段 */;
}
interface NameLabel {
name: string /* 另一些字段 */;
}
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
throw "unimplemented";
}
createLabel 的这些重载描述了一个单一的JavaScript函数,该函数根据其输入的类型做出选择。注意 一些事情:
- 如果一个库必须在其API中反复做出同样的选择,这就会变得很麻烦。
- 我们必须创建三个重载:一个用于确定类型的情况(一个用于 string ,一个用于 number ),一 个用于最一般的情况(取一个 string | number )。对于 createLabel 所能处理的每一种新类 型,重载的数量都会呈指数级增长。
相反,我们可以在一个条件类型中对该逻辑进行编码:
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
然后我们可以使用该条件类型,将我们的重载简化为一个没有重载的单一函数。
interface IdLabel {
id: number /* some fields */;
}
interface NameLabel {
name: string /* other fields */;
}
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
// let a: NameLabel
let a = createLabel("typescript");
// let b: IdLabel
let b = createLabel(2.8);
// let c: NameLabel | IdLabel
let c = createLabel(Math.random() ? "hello" : 42);
条件类型约束
通常,条件类型中的检查会给我们提供一些新的信息。就像用类型守卫缩小范围可以给我们一个更具体 的类型一样,条件类型的真正分支将通过我们检查的类型进一步约束泛型。
例如,让我们来看看下面的例子:
type MessageOf<T> = T["message"];
在这个例子中,TypeScript出错是因为 T 不知道有一个叫做 message 的属性。我们可以对 T 进行约 束,TypeScript就不会再抱怨。
type MessageOf<T extends { message: unknown }> = T["message"];
interface Email {
message: string;
}
type EmailMessageContents = MessageOf<Email>;
然而,如果我们想让 MessageOf 接受任何类型,并在消息属性不可用的情况下,默认为 never 类型 呢?我们可以通过将约束条件移出,并引入一个条件类型来做到这一点。
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
// type EmailMessageContents = string
type EmailMessageContents = MessageOf<Email>;
const emc:EmailMessageContents = 'balabala...'
// type DogMessageContents = never
type DogMessageContents = MessageOf<Dog>;
const dmc:DogMessageContents = 'error' as never
在真正的分支中,TypeScript知道 T 会有一个消息属性。
作为另一个例子,我们也可以写一个叫做 Flatten 的类型,将数组类型平铺到它们的元素类型上,但 在其他方面则不做处理。
type Flatten<T> = T extends any[] ? T[number] : T;
// 提取出元素类型。
// type Str = string
type Str = Flatten<string[]>;
// 单独一个类型。
// type Num = number
type Num = Flatten<number>;
当 Flatten 被赋予一个数组类型时,它使用一个带有数字的索引访问来获取 string[] 的元素类型。 否则,它只是返回它被赋予的类型。
在条件类型内进行推理
我们只是发现自己使用条件类型来应用约束条件,然后提取出类型。这最终成为一种常见的操作,而条 件类型使它变得更容易。
条件类型为我们提供了一种方法来推断我们在真实分支中使用 infer 关键字进行对比的类型。例如, 我们可以在 Flatten 中推断出元素类型,而不是用索引访问类型 "手动 "提取出来。
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
在这里,我们使用 infer 关键字来声明性地引入一个名为 Item 的新的通用类型变量,而不是指定如 何在真实分支中检索 T 的元素类型。这使我们不必考虑如何挖掘和探测我们感兴趣的类型的结构。
我们可以使用 infer 关键字编写一些有用的辅助类型别名。例如,对于简单的情况,我们可以从函数 类型中提取出返回类型。
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
? Return
: never;
// type Num = number
type Num = GetReturnType<() => number>;
// type Str = string
type Str = GetReturnType<(x: string) => string>;
// type Bools = boolean[]
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
// 给泛型传入 string 类型,条件类型会返回 never
type Never = GetReturnType<string>
const nev:Never = 'error' as never
当从一个具有多个调用签名的类型(如重载函数的类型)进行推断时,从最后一个签名进行推断(据推 测,这是最容许的万能情况)。不可能根据参数类型的列表来执行重载解析。
declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;
// type T1 = string | number
type T1 = ReturnType<typeof stringOrNum>;
这里发生的情况是,StrArrOrNumArr分布在:
string | number;
并对联合的每个成员类型进行映射,以达到有效的目的:
ToArray<string> | ToArray<number>;
这给我们留下了:
string[] | number[];
通常情况下,分布性是需要的行为。为了避免这种行为,你可以用方括号包围 extends 关键字的每一 边。
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
// 'StrArrOrNumArr'不再是一个联合类型
// type StrArrOrNumArr = (string | number)[]
type StrArrOrNumArr = ToArrayNonDist<string | number>;
映射类型
当你不想重复定义类型,一个类型可以以另一个类型为基础创建新类型。
映射类型建立在索引签名的语法上,索引签名用于声明没有被提前声明的属性类型。
type OnlyBoolsAndHorses = {
[key: string]: boolean | Horse;
};
const conforms: OnlyBoolsAndHorses = {
del: true,
rodney: false,
};
映射类型是一种通用类型,它使用 PropertyKeys 的联合(经常通过 keyof 创建)迭代键来创建一个类型。
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
在这个例子中, OptionsFlags 将从 Type 类型中获取所有属性,并将它们的值改为布尔值。
type FeatureFlags = {
darkMode: () => void;
newUserProfile: () => void;
};
/*
type FeatureOptions = {
darkMode: boolean;
newUserProfile: boolean;
}
*/
type FeatureOptions = OptionsFlags<FeatureFlags>;
映射修改器
在映射过程中,有两个额外的修饰符可以应用: readonly 和 ? ,它们分别影响可变性和可选性。
你可以通过用 - 或 + 作为前缀来删除或添加这些修饰语。如果你不加前缀,那么就假定是 + 。
type CreateMutable<Type> = {
// 从一个类型的属性中删除 "readonly"属性
-readonly [Property in keyof Type]: Type[Property];
};
type LockedAccount = {
readonly id: string;
readonly name: string;
};
/*
type UnlockedAccount = {
id: string;
name: string;
}
*/
type UnlockedAccount = CreateMutable<LockedAccount>;
// 从一个类型的属性中删除 "可选" 属性
type Concrete<Type> = {
[Property in keyof Type]-?: Type[Property];
};
type MaybeUser = {
id: string;
name?: string;
age?: number;
};
/*
type User = {
id: string;
name: string;
age: number;
}
*/
type User = Concrete<MaybeUser>;
通过 as 做 key 重映射
在TypeScript 4.1及以后的版本中,你可以通过映射类型中的as子句重新映射映射类型中的键。
type MappedTypeWithNewProperties<Type> = {
[Properties in keyof Type as NewKeyType]: Type[Properties]
}
你可以利用模板字面类型等功能,从先前的属性名称中创建新的属性名称。
type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () =>
Type[Property]
};
interface Person {
name: string;
age: number;
location: string;
}
/*
type LazyPerson = {
getName: () => string;
getAge: () => number;
getLocation: () => string;
}
*/
type LazyPerson = Getters<Person>;
你可以通过条件类型产生 never 滤掉的键。
// 删除 "kind"属性
type RemoveKindField<Type> = {
[Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
};
/*
type KindlessCircle = {
radius: number;
}
*/
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
你可以映射任意的联合体,不仅仅是 string | number | symbol 的联合体,还有任何类型的联合体。
type EventConfig<Events extends { kind: string }> = {
[E in Events as E["kind"]]: (event: E) => void;
}
type SquareEvent = { kind: "square", x: number, y: number };
type CircleEvent = { kind: "circle", radius: number };
/*
type Config = {
square: (event: SquareEvent) => void;
circle: (event: CircleEvent) => void;
}
*/
type Config = EventConfig<SquareEvent | CircleEvent>
进一步探索
映射类型与本类型操作部分的其他功能配合得很好,例如,这里有一个使用条件类型的映射类型 ,它根 据一个对象的属性 pii 是否被设置为字面意义上的 true ,返回 true 或 false 。
type ExtractPII<Type> = {
[Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
};
/*
type ObjectsNeedingGDPRDeletion = {
id: false;
name: true;
}
*/
type DBFields = {
id: { format: "incrementing" };
name: { type: string; pii: true };
};
type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>
“如果你爱一个就不要害怕结局,如果你付出的是真心,那一定是一个美好的结局,就算没有走到最后,那最后后悔的人一定不是你。”