使用 keyof 约束对象
其中使用了 TS 泛型和泛型约束。首先定义了 T 类型并使用 extends 关键字继承 object 类型的子类型,然后使用 keyof 操作符获取 T 类型的所有键,它的返回 类型是联合 类型,最后利用 extends 关键字约束 K 类型必须为 keyof T 联合类型的子类型
functionprop<T, KextendskeyofT>(obj: T, key: K) {
returnobj[key]
}
leto= { a: 1, b: 2, c: 3 }
prop(o, 'a')
prop(o, 'd') //,我们需要约束一下这个o里面并没有的东西,此时就会报错发现找不到
//通过提示,我门可以看到类型"d"的参数不能赋给类型"a"|"b"|"c"的参数
泛型类
声明方法跟函数类似名称后面定义 <类型>
使用的时候确定类型 new Sub<number>()
//定义泛型的一个类
classSub<T>{
attr:T[] = []//这里的:只是普通的:
add(a:T):T[]{
return [a]
}
}
lets=newSub<number>()//这里已经使用泛型固定为number了
s.attr= [123]//正常运行
s.attr= ['123']//报错
s.add(123)//也是只能传数字
letstr=newSub<string>()//这里已经使用泛型固定为number了
str.attr= [123]//报错
str.attr= ['123']//正常运行
str.add('123')//也是只能传字符串
console.log(s,str)
泛型工具类型(大量补充额外内容)
作者使用了Typora作为写笔记的编辑器,这里可以对目录进行折叠方面我们查阅我们想要的部分
为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。不过在具体介绍之前,我们得先介绍一些相关的基础知识,方便读者可以更好的学习其它的工具类型。
1.typeof
typeof 的主要用途是在类型上下文中获取变量或者属性的类型,下面我们通过一个具体示例来理解一下。
interfacePerson {
name: string;
age: number;
}
constsem: Person= { name: "semlinker", age: 30 };
typeSem=typeofsem; // type Sem = Person
在上面代码中,我们通过 typeof
操作符获取 sem 变量的类型并赋值给 Sem 类型变量,之后我们就可以使用 Sem 类型:
constlolo: Sem= { name: "lolo", age: 5 }
你也可以对嵌套对象执行相同的操作:
constMessage= {
name: "jimmy",
age: 18,
address: {
province: '四川',
city: '成都'
}
}
typemessage=typeofMessage;
/*
type message = {
name: string;
age: number;
address: {
province: string;
city: string;
};
}
*/
此外,typeof
操作符除了可以获取对象的结构类型之外,它也可以用来获取函数对象的类型,比如:
functiontoArray(x: number): Array<number> {
return [x];
}
typeFunc=typeoftoArray; // -> (x: number) => number[]
2.keyof
keyof
操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。
interfacePerson {
name: string;
age: number;
}
typeK1=keyofPerson; // "name" | "age"
typeK2=keyofPerson[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
typeK3=keyof { [x: string]: Person }; // string | number
在 TypeScript 中支持两种索引签名,数字索引和字符串索引:
interfaceStringArray {
// 字符串索引 -> keyof StringArray => string | number
[index: string]: string;
}
interfaceStringArray1 {
// 数字索引 -> keyof StringArray1 => number
[index: number]: string;
}
为了同时支持两种索引类型,就得要求数字索引的返回值必须是字符串索引返回值的子类。其中的原因就是当使用数值索引时,JavaScript 在执行索引操作时,会先把数值索引先转换为字符串索引。所以 keyof { [x: string]: Person }
的结果会返回 string | number
。
keyof 也支持基本数据类型:
let K1: keyof boolean; // let K1: "valueOf"
let K2: keyof number; // let K2: "toString" | "toFixed" | "toExponential" | ...
let K3: keyof symbol; // let K1: "valueOf"
keyof 的作用
JavaScript 是一种高度动态的语言。有时在静态类型系统中捕获某些操作的语义可能会很棘手。以一个简单的 prop
函数为例:
functionprop(obj, key) {
returnobj[key];
}
该函数接收 obj 和 key 两个参数,并返回对应属性的值。对象上的不同属性,可以具有完全不同的类型,我们甚至不知道 obj 对象长什么样。
那么在 TypeScript 中如何定义上面的 prop
函数呢?我们来尝试一下:
functionprop(obj: object, key: string) {
returnobj[key];
}
在上面代码中,为了避免调用 prop 函数时传入错误的参数类型,我们为 obj 和 key 参数设置了类型,分别为 {}
和 string
类型。然而,事情并没有那么简单。针对上述的代码,TypeScript 编译器会输出以下错误信息:
Elementimplicitlyhasan'any'typebecauseexpressionoftype'string'can't be used to index type '{}'.
元素隐式地拥有 any
类型,因为 string
类型不能被用于索引 {}
类型。要解决这个问题,你可以使用以下非常暴力的方案:
functionprop(obj: object, key: string) {
return (objasany)[key];
}
很明显该方案并不是一个好的方案,我们来回顾一下 prop
函数的作用,该函数用于获取某个对象中指定属性的属性值。因此我们期望用户输入的属性是对象上已存在的属性,那么如何限制属性名的范围呢?这时我们可以利用本文的主角 keyof
操作符:
functionprop<Textendsobject, KextendskeyofT>(obj: T, key: K) {
returnobj[key];
}
在以上代码中,我们使用了 TypeScript 的泛型和泛型约束。首先定义了 T 类型并使用 extends
关键字约束该类型必须是 object 类型的子类型,然后使用 keyof
操作符获取 T 类型的所有键,其返回类型是联合类型,最后利用 extends
关键字约束 K 类型必须为 keyof T
联合类型的子类型。 是骡子是马拉出来遛遛就知道了,我们来实际测试一下:
typeTodo= {
id: number;
text: string;
done: boolean;
}
consttodo: Todo= {
id: 1,
text: "Learn TypeScript keyof",
done: false
}
functionprop<Textendsobject, KextendskeyofT>(obj: T, key: K) {
returnobj[key];
}
constid=prop(todo, "id"); // const id: number
consttext=prop(todo, "text"); // const text: string
constdone=prop(todo, "done"); // const done: boolean
很明显使用泛型,重新定义后的 prop<T extends object, K extends keyof T>(obj: T, key: K)
函数,已经可以正确地推导出指定键对应的类型。那么当访问 todo 对象上不存在的属性时,会出现什么情况?比如:
constdate=prop(todo, "date");
对于上述代码,TypeScript 编译器会提示以下错误:
Argumentoftype'"date"'isnotassignabletoparameteroftype'"id" | "text" | "done"'.
这就阻止我们尝试读取不存在的属性。
3.in
in
用来遍历枚举类型:
typeKeys="a"|"b"|"c"
typeObj= {
[pinKeys]: any
} // -> { a: any, b: any, c: any }
4.infer
在条件类型语句中,可以用 infer
声明一个类型变量并且对它进行使用。
typeReturnType<T>=Textends (
...args: any[]
) =>inferR?R : any;
以上代码中 infer R
就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。
5.extends
有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 关键字添加泛型约束。
interfaceLengthwise {
length: number;
}
functionloggingIdentity<TextendsLengthwise>(arg: T): T {
console.log(arg.length);
returnarg;
}
现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:
loggingIdentity(3); // Error, number doesn't have a .length property
这时我们需要传入符合约束类型的值,必须包含 length 属性:
loggingIdentity({length: 10, value: 3});
索引类型
在实际开发中,我们经常能遇到这样的场景,在对象中获取一些属性的值,然后建立对应的集合。
letperson= {
name: 'musion',
age: 35
}
functiongetValues(person: any, keys: string[]) {
returnkeys.map(key=>person[key])
}
console.log(getValues(person, ['name', 'age'])) // ['musion', 35]
console.log(getValues(person, ['gender'])) // [undefined]
在上述例子中,可以看到 getValues (persion, ['gender']) 打印出来的是 [undefined],但是 ts 编译器并没有给出报错信息,那么如何使用 ts 对这种模式进行类型约束呢?这里就要用到了索引类型,改造一下 getValues 函数,通过 索引类型查询和 索引访问 操作符:
functiongetValues<T, KextendskeyofT>(person: T, keys: K[]): T[K][] {
returnkeys.map(key=>person[key]);
}
interfacePerson {
name: string;
age: number;
}
constperson: Person= {
name: 'musion',
age: 35
}
getValues(person, ['name']) // ['musion']
getValues(person, ['gender']) // 报错:
// Argument of Type '"gender"[]' is not assignable to parameter of type '("name" | "age")[]'.
// Type "gender" is not assignable to type "name" | "age".
编译器会检查传入的值是否是 Person 的一部分。通过下面的概念来理解上面的代码:
T[K]表示对象T的属性K所表示的类型,在上述例子中,T[K][] 表示变量T取属性K的值的数组
// 通过[]索引类型访问操作符, 我们就能得到某个索引的类型
classPerson {
name:string;
age:number;
}
typeMyType=Person['name']; //Person中name的类型为string type MyType = string
介绍完概念之后,应该就可以理解上面的代码了。首先看泛型,这里有 T 和 K 两种类型,根据类型推断,第一个参数 person 就是 person,类型会被推断为 Person。而第二个数组参数的类型推断(K extends keyof T),keyof 关键字可以获取 T,也就是 Person 的所有属性名,即 ['name', 'age']。而 extends 关键字让泛型 K 继承了 Person 的所有属性名,即 ['name', 'age']。这三个特性组合保证了代码的动态性和准确性,也让代码提示变得更加丰富了
getValues(person, ['gender']) // 报错:
// Argument of Type '"gender"[]' is not assignable to parameter of type '("name" | "age")[]'.
// Type "gender" is not assignable to type "name" | "age".
映射类型
根据旧的类型创建出新的类型,我们称之为映射类型
比如我们定义一个接口
interfaceTestInterface{
name:string,
age:number
}
我们把上面定义的接口里面的属性全部变成可选
// 我们可以通过+/-来指定添加还是删除
typeOptionalTestInterface<T>= {
[pinkeyofT]+?:T[p]
}
typenewTestInterface=OptionalTestInterface<TestInterface>
// type newTestInterface = {
// name?:string,
// age?:number
// }
比如我们再加上只读
typeOptionalTestInterface<T>= {
+readonly [pinkeyofT]+?:T[p]
}
typenewTestInterface=OptionalTestInterface<TestInterface>
// type newTestInterface = {
// readonly name?:string,
// readonly age?:number
// }
由于生成只读属性和可选属性比较常用,所以 TS 内部已经给我们提供了现成的实现 Readonly / Partial, 会面内置的工具类型会介绍.
内置的工具类型
Partial
Partial<T>
将类型的属性变成可选
定义
typePartial<T>= {
[PinkeyofT]?: T[P];
};
在以上代码中,首先通过 keyof T
拿到 T
的所有属性名,然后使用 in
进行遍历,将值赋给 P
,最后通过 T[P]
取得相应的属性值的类。中间的 ?
号,用于将所有属性变为可选。
举例说明
interfaceUserInfo {
id: string;
name: string;
}
// error:Property 'id' is missing in type '{ name: string; }' but required in type 'UserInfo'
constxiaoming: UserInfo= {
name: 'xiaoming'
}
使用 Partial<T>
typeNewUserInfo=Partial<UserInfo>;
constxiaoming: NewUserInfo= {
name: 'xiaoming'
}
这个 NewUserInfo 就相当于
interfaceNewUserInfo {
id?: string;
name?: string;
}
但是 Partial<T>
有个局限性,就是只支持处理第一层的属性,如果我的接口定义是这样的
interfaceUserInfo {
id: string;
name: string;
fruits: {
appleNumber: number;
orangeNumber: number;
}
}
typeNewUserInfo=Partial<UserInfo>;
// Property 'appleNumber' is missing in type '{ orangeNumber: number; }' but required in type '{ appleNumber: number; orangeNumber: number; }'.
constxiaoming: NewUserInfo= {
name: 'xiaoming',
fruits: {
orangeNumber: 1,
}
}
可以看到,第二层以后就不会处理了,如果要处理多层,就可以自己实现
DeepPartial
typeDeepPartial<T>= {
// 如果是 object,则递归类型
[UinkeyofT]?: T[U] extendsobject
?DeepPartial<T[U]>
: T[U]
};
typePartialedWindow=DeepPartial<T>; // 现在T上所有属性都变成了可选啦
Required
Required 将类型的属性变成必选
定义
typeRequired<T>= {
[PinkeyofT]-?: T[P]
};
其中 -?
是代表移除 ?
这个 modifier 的标识。再拓展一下,除了可以应用于 ?
这个 modifiers ,还有应用在 readonly
,比如 Readonly<T>
这个类型
typeReadonly<T>= {
readonly [pinkeyofT]: T[p];
}
Readonly
Readonly<T>
的作用是将某个类型所有属性变为只读属性,也就意味着这些属性不能被重新赋值。
定义
typeReadonly<T>= {
readonly [PinkeyofT]: T[P];
};
举例说明
interfaceTodo {
title: string;
}
consttodo: Readonly<Todo>= {
title: "Delete inactive users"
};
todo.title="Hello"; // Error: cannot reassign a readonly property
Pick
Pick 从某个类型中挑出一些属性出来
定义
typePick<T, KextendskeyofT>= {
[PinK]: T[P];
};
举例说明
interfaceTodo {
title: string;
description: string;
completed: boolean;
}
typeTodoPreview=Pick<Todo, "title"|"completed">;
consttodo: TodoPreview= {
title: "Clean room",
completed: false,
};
可以看到 NewUserInfo 中就只有个 name 的属性了。
Record
Record<K extends keyof any, T>
的作用是将K
中所有的属性的值转化为T
类型。
定义
typeRecord<Kextendskeyofany, T>= {
[PinK]: T;
};
举例说明
interfacePageInfo {
title: string;
}
typePage="home"|"about"|"contact";
constx: Record<Page, PageInfo>= {
about: { title: "about" },
contact: { title: "contact" },
home: { title: "home" },
};
ReturnType
用来得到一个函数的返回值类型
定义
typeReturnType<Textends (...args: any[]) =>any>=Textends (
...args: any[]
) =>inferR
?R
: any;
infer
在这里用于提取函数类型的返回值类型。ReturnType<T>
只是将 infer R 从参数位置移动到返回值位置,因此此时 R 即是表示待推断的返回值类型。
举例说明
typeFunc= (value: number) =>string;
constfoo: ReturnType<Func>="1";
ReturnType
获取到 Func
的返回值类型为 string
,所以,foo
也就只能被赋值为字符串了。
Exclude
Exclude<T, U>
的作用是将某个类型中属于另一个的类型移除掉。
定义
typeExclude<T, U>=TextendsU?never : T;
如果 T
能赋值给 U
类型的话,那么就会返回 never
类型,否则返回 T
类型。最终实现的效果就是将 T
中某些属于 U
的类型移除掉。
举例说明
typeT0=Exclude<"a"|"b"|"c", "a">; // "b" | "c"
typeT1=Exclude<"a"|"b"|"c", "a"|"b">; // "c"
typeT2=Exclude<string|number| (() =>void), Function>; // string | number
Extract
Extract<T, U>
的作用是从T
中提取出U
。
定义
typeExtract<T, U>=TextendsU?T : never;
举例说明
typeT0=Extract<"a"|"b"|"c", "a"|"f">; // "a"
typeT1=Extract<string|number| (() =>void), Function>; // () =>void
Omit
Omit<T, K extends keyof any>
的作用是使用T
类型中除了K
类型的所有属性,来构造一个新的类型。
定义
typeOmit<T, Kextendskeyofany>=Pick<T, Exclude<keyofT, K>>;
举例说明
interfaceTodo {
title: string;
description: string;
completed: boolean;
}
typeTodoPreview=Omit<Todo, "description">;
consttodo: TodoPreview= {
title: "Clean room",
completed: false,
};
NonNullable
NonNullable<T>
的作用是用来过滤类型中的null
及undefined
类型。
定义
typeNonNullable<T>=Textendsnull|undefined?never : T;
举例说明
typeT0=NonNullable<string|number|undefined>; // string | number
typeT1=NonNullable<string[] |null|undefined>; // string[]
Parameters
Parameters<T>
的作用是用于获得函数的参数类型组成的元组类型。
定义
typeParameters<Textends (...args: any) =>any>=Textends (...args: inferP) =>any
?P : never;
举例说明
typeA=Parameters<() =>void>; // []
typeB=Parameters<typeofArray.isArray>; // [any]
typeC=Parameters<typeofparseInt>; // [string, (number | undefined)?]
typeD=Parameters<typeofMath.max>; // number[]