最近博主一直在创作
TypeScript
的内容,所有的TypeScript
文章都在我的TypeScript专栏里,每一篇文章都是精心打磨的优质好文,并且非常的全面和细致,期待你的订阅
TypeScript
的类型系统允许用其他类型的术语来表达类型。
通过结合各种类型操作符,我们可以用一种简洁、可维护的方式来表达复杂的操作和值。在本篇文章中,我们将介绍用现有的类型或值来表达一个新类型的方法:
Keyof
类型操作符: keyof
操作符创建新类型Typeof
类型操作符 : 使用 typeof
操作符来创建新的类型Type['a']
语法来访问一个类型的子集在TypeScript专栏中的前几篇文章中,我们以及大致了解了泛型的基本使用,见:
在这一节中我们将对泛型进行进一步的补充
在【TypeScript】深入学习TypeScript函数泛型函数(通用函数) 中我们创建了在一系列类型上工作的通用函数,在这一节中,我们将探讨函数本身的类型以及如何创建通用接口
泛型函数的类型与非泛型函数的类型一样,类型参数列在前面,与函数声明类似:
(param:TypeToParamType) => TypeToReturnType
(param:paramType) => returnType
先看一个我们之前定义过的一个通用函数:
function getFirstElement<Type>(arr: Type[]): Type | undefined {
return arr[0];
}
它的类型就是
,我们可以将它赋值给同类型的函数fn
:
let fn: <Type>(arr: Type[]) => Type | undefined = getFirstElement;
console.log(fn<number>([1, 2, 3]));
我们也可以为类型中的通用类型参数使用一个不同的名字,只要类型变量的数量和类型变量的使用方式一致即可:
let fn: <FnType>(fnArr: FnType[]) => FnType | undefined = getFirstElement;
console.log(fn<number>([1, 2, 3]));
我们也可以把泛型写成一个对象字面类型的调用签名:
let fn: { <FnType>(fnArr: FnType[]): FnType | undefined } = getFirstElement;
console.log(fn<number>([1, 2, 3]));
这时可以将对象字面类型移到一个接口中:
interface Ifn {
<FnType>(fnArr: FnType[]): FnType | undefined;
}
let fn: Ifn = getFirstElement;
console.log(fn<number>([1, 2, 3]));
在一些情况下,我们还可以将通用参数移到整个接口的参数上,这使得我们可以看到我们的泛型是什么类型(例如Ifn
而不仅仅是Ifn
),使得类型参数对接口的所有其它成员可见:
interface Ifn<FnType> {
(fnArr: FnType[]): FnType | undefined;
}
let strFn: Ifn<string> = getFirstElement;
console.log(strFn(["1", "2", "3"]));
console.log(strFn([1, 2, 3])); // err:不能将类型“number”分配给类型“string”
注意:这里的例子已经变了,不再是简单的将
getFirstElement
函数直接赋值给另一个函数,而是将类型参数为string
的getFirstElement
函数赋值给strFn
上述strFn
相当于fn
:
interface Ifn {
<FnType>(fnArr: FnType[]): FnType | undefined;
}
let fn: Ifn = getFirstElement;
console.log(fn<string>(["1", "2", "3"]));
泛型类在类的名字后面有一个角括号(<>
)中的泛型参数列表:
class Add<AddType> {
initVal: AddType| undefined;
add: ((x: AddType, y: AddType) => AddType) | undefined;
}
使用:
let myNumber = new Add<number>();
myNumber.initVal = 1;
myNumber.add = function (x, y) {
return x + y;
};
console.log(myNumber.add(myNumber.initVal, 18)); // 19
let myStr = new Add<string>();
myStr.initVal = "Ailjx";
myStr.add = function (x, y) {
return x + y;
};
console.log(myStr.add(myStr.initVal, " OK")); // Ailjx OK
就像接口一样,把类型参数放在类本身,可以让我们确保类的所有属性都与相同的类型一起工作。
注意:一个类的类型有两个方面:静态方面和实例方面。通用类只在其实例侧而非静态侧具有通用性,所以在使用类时,静态成员不能使用类的类型参数。
在【TypeScript】深入学习TypeScript函数泛型函数(通用函数) 中我们已经了解过了使用extends
约束泛型,这一节我们继续深入
在泛型约束中使用类型参数
你可以声明一个受另一个类型参数约束的类型参数。
例如,我们想从一个给定名称的对象中获取一个属性。我们想确保我们不会意外地获取一个不存在于 obj
上的属性,所以我们要在这两种类型之间放置一个约束条件:
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
keyof
运算符接收一个对象类型,并产生其键的字符串或数字字面联合,下面会详细讲解
在TypeScript
中使用泛型创建工厂时,有必要通过其构造函数来引用类的类型,比如说:
function create<Type>(c: new () => Type): Type {
return new c();
}
create
函数代表接收一个构造函数,并返回其实例
参数c
的类型使用的是构造签名,表示其接收一个构造函数,并且该构造函数实例的类型(Type
)被当作了create
函数的类型参数并在其它地方进行使用,如create
的返回值类型就是引用了Type
一个更高级的例子,使用原型属性来推断和约束类类型的构造函数和实例方之间的关系:
class Animal {
numLegs: number = 4;
}
class Bee extends Animal {
name: string = "Bee";
getName() {
console.log(this.name);
}
}
class Lion extends Animal {
name: string = "Lion";
getName() {
console.log(this.name);
}
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Bee).getName(); // Bee
createInstance(Lion).getName(); // Lion
这里的createInstance
函数表示只能接收一个实例类型受限于Animal
的构造函数,并返回其实例
keyof
运算符接收一个对象类型,并产生其键的字符串或数字字面联合:
type ObjType = { x: number; y: number };
const p1: keyof ObjType = "x";
// 相当于
// const p1: "x" | "y" = "x";
如果该类型有一个字符串或数字索引签名, keyof
将返回这些类型:
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish; // A为 number
const a: A = 1;
type Mapish = { [k: string]: boolean };
type M = keyof Mapish; // M为 string|number
const m: M = "a";
const m2: M = 10;
注意:在这个例子中,
M
是string|number
——这是因为JavaScript
对象的键总是被强制为字符串,所以obj[0]
总是与obj["0"]
相同。
在JavaScript
中可以使用typeof
操作符获取某一变量的类型,在TypeScript
中我们可以使用它来在类型上下文中引用一个变量或属性的类型:
let s = "hello";
let n: typeof s; // n类型为string
n = "world";
n = 100; // err:不能将类型“number”分配给类型“string”
结合其他类型操作符,你可以使用typeof
来方便地表达许多模式。
例如我们想要获取函数返回值的类型:
TypeScript
中内置的类型ReturnType
接收一个函数类型并产生其返回类型:
type Predicate = (x: unknown) => boolean;
type K = ReturnType<Predicate>; // k为boolean
如果直接在一个函数名上使用 ReturnType
,我们会看到一个指示性的错误:
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<typeof f>; // P为{ x: number, y: number }
只有在标识符(即变量名)或其属性上使用
typeof
是合法的
可以使用一个索引访问类型来查询一个类型上的特定属性的类型:
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; // Age类型为number
还可以配合联合类型 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;
将索引访问类型和typeof
,number
结合起来,方便地获取一个数组字面的元素类型:
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;
注意:
在TypeScript
我们可以使用三元表达式来判断一个类型:
interface Animal {}
interface Dog extends Animal {}
// type Example1 = number
type Example1 = Dog extends Animal ? number : string;
// type Example2 = string
type Example2 = RegExp extends Animal ? number : string;
条件类型表达式是通过extends
进行约束和判断
先看一个简单的例子:
type Flatten<T> = T extends any[] ? T[number] : T;
// 提取出元素类型。
// type Str = string
type Str = Flatten<string[]>;
// 单独一个类型。
// type Num = number
type Num = Flatten<number>;
当 Flatten
被赋予一个数组类型时,它使用一个带有数字的索引访问来获取数组的元素类型。否则,它只是返回它被赋予的类型。
在看一个复杂的例子,实现一个获取id
或name
的对象格式的函数getIdOrNameObj
:
interface IId {
id: number;
}
interface IName {
name: string;
}
// 条件类型配合泛型对类型进行判断和选择
type IdOrName<T extends number | string> = T extends number ? IId : IName;
function getIdOrNameObj<T extends number | string>(idOrName: T): IdOrName<T> {
if (typeof idOrName === "number") {
return {
id: idOrName,
} as IdOrName<T>;
} else {
return {
name: idOrName,
} as IdOrName<T>;
}
}
const myId = getIdOrNameObj(1); // myId类型为IId
const myName = getIdOrNameObj("Ailjx"); // myName类型为IName
```### 类型推理
在条件类型的 `extends`子句中我们可以使用 `infer` 声明来推断元素类型
> `infer` 声明只能在条件类型的 `extends`子句中使用
例如,我们使用`infer` 关键字来改写上面的`Flatten`:
```typescript
type Flatten<T> = T extends Array<infer Item> ? Item : T;
// type Str = string
type Str = Flatten<string[]>;
// type Str = number
type Num = Flatten<number[]>;
这里使用
infer
关键字来声明性地引入一个名为Item
的新的通用类型变量
这里
infer Item
相当于一个占位,它暂时代表Array
中元素的类型,当Flatten
类型参数被赋值为数组后,TypeScript
就会自动推断出extends
语句中Array
中元素的类型,这时infer Item
这个占位就指向了数组元素的类型,之后就能直接使用Item
来代指数组元素的类型了
这使得我们不用再使用索引访问类型
T[number]
"手动 "提取数组元素的类型了
使用 infer
关键字从函数类型中提取出返回类型:
// 当GetReturnType接收类型为函数签名时返回函数返回值类型,否者直接返回接收的类型
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
? Return
: Type;
// 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[]>;
// type Arr=any[]
type Arr = GetReturnType<any[]>;
当从一个具有多个调用签名的类型(如重载函数的类型)进行推断时,从最后一个签名进行推断:
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>;
declare
可以向TypeScript
域中引入一个变量,这可以解决在重载函数只有重载签名而没有实现签名时的报错
当条件类型作用于一个通用类型时,当给定一个联合类型时,它就变成了分布式的:
type ToArray<Type> = Type extends any ? Type[] : never;
// type StrArrOrNumArr = string[] | number[]
type StrArrOrNumArr = ToArray<string | number>;
将一个联合类型string | number
插入ToArray
,那么条件类型将被应用于该联合的每个成员
StrArrOrNumArr
分布在string | number
;ToArray | ToArray
string[] | number[]
取消分布式:
如果不需要分布式的这种行为,我们可以使用方括号[]
包围extends
关键字的两边
type ToArray<Type> = [Type] extends [any] ? Type[] : never;
// type StrArrOrNumArr = (string|number)[]
type StrArrOrNumArr = ToArray<string | number>;
当一个类型可以以另一个类型为基础创建新类型。
映射类型建立在索引签名(见【TypeScript】深入学习TypeScript对象类型)的语法上
映射类型是一种通用类型,它使用 PropertyKeys
的联合(经常通过keyof
创建)迭代键来创建一个类型:
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
在这个例子中, OptionsFlags
将从Type
类型中获取所有属性,并将它们的值的类型改为boolean
:
type Obj = {
name: string;
age: number;
};
type FeatureOptions = OptionsFlags<Obj>;
/*
type FeatureOptions = {
name: boolean;
age: boolean;
}
*/
在映射过程中,有两个额外的修饰符可以应用: readonly
和?
,它们分别影响可变性和可选性,可以通过用-
或+
作为前缀来删除或添加这些修饰符(不加修饰符就默认是+
):
type OptionsFlags<Type> = {
// 删除readonly和?,readonly在前,?在后
-readonly [Property in keyof Type]-?: boolean;
};
type Obj = {
readonly name: string;
age?: number;
};
type FeatureOptions = OptionsFlags<Obj>;
/*
type FeatureOptions = {
name: boolean;
age: boolean;
}
*/
在TypeScript 4.1
及以后的版本中,可以通过映射类型中的as
子句修改映射类型中的键名:
type OptionsFlags<Type> = {
// 将键重命名为哦、
[Property in keyof Type as "o"]: Type[Property];
};
type Obj = {
name: string;
age: number;
};
type FeatureOptions = OptionsFlags<Obj>;
/*
type FeatureOptions = {
o:string|number
}
*/
上面是将所有键名都更改成了'o'
我们也可以利用模板字面类型,在之前的属性名称的基础上进行更改:
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>;
Capitalize
为TS
内置类型,能将传入的字符串类型首字母转为大写string & Property
通过交叉类型,确保Capitalize
接收的为字符串类型可以通过条件类型Exclude
根据键名产生never
,从而过滤掉该键:
type RemoveKindField<Type> = {
[Property in keyof Type as Exclude<Property, "kind">]: Type[Property];
};
interface Circle {
kind: "circle";
radius: number;
}
/*
type KindlessCircle = {
radius: number;
}
*/
type KindlessCircle = RemoveKindField<Circle>;
Exclude
为TS
内置类型:type Exclude
可以映射任意的联合体:
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>;
映射类型与本篇文章中指出的其他功能配合得很好,例如,下面这个使用条件类型的映射类型,它根据一个对象类型的属性show
是否被设置为字面类型true
而返回true
或false
:
type ExtractShow<Type> = {
[Property in keyof Type]: Type[Property] extends { show: true }
? true
: false;
};
type PermissionInfo = {
home: { url: string; show: true };
about: { url: string; show: true };
admin: { url: string };
};
/*
type judge = {
home: true;
about: true;
admin: false;
}
*/
type judge = ExtractShow<PermissionInfo>;
至此,本篇文章内容就全部结束了,如果本篇文章对你有所帮助,还请客官一件四连!
博主的TypeScript专栏正在慢慢的补充之中,赶快关注订阅,与博主一起进步吧!期待你的三连支持。
参考资料:TypeScript官网