TypeScript 类型保护、索引类型、类型映射、条件类型
一、类型保护
类型保护又叫类型守卫,是为了提高程序的鲁棒性存在的。先来看一个例子:
enum Type { JavaScript , Python };
class JavaScript {
helloJS(){
console.log("hello JavaScript!");
}
}
class Deno {
helloDeno(){
console.log("hello deno!");
}
}
function getLangage(type:Type):void{
const lang = type === 0 ? new JavaScript() : new Deno();
if(lang.helloJS){//报错
lang.helloJS();
}else{
lang.helloDeno();
}
console.log(lang);
}
getLangage(Type.JavaScript);
上面的代码 lang 使用三元来赋值的,lang 是一个联合类型,const lang: JavaScript | Deno
无法准确确定 lang 的值,所以条件语句里的代码为了防止出错,应为使用类型断言。条件语句的代码改写为:
if((<JavaScript>lang).helloJS){
(<JavaScript>lang).helloJS();
}else{
(<Deno>lang).helloDeno();
}
现在没问题了,但是每个地方我们都加了类型断言,很明显这种方案不是我们想要的可读性差写起来也是很麻烦类型守卫就是解决这个问题的。可以简单理解为类型保护就是解决类型断言可读性差和书写不方便的。
类型保护:能够在特定的区块中(比如函数里)保证变量属于某种特定的类型,可以在区块中放心的使用此类型的属性或方法。
下面介绍四种,还使用上面的案例,只改动条件判断语句:
- instanceof
if(lang instanceof JavaScript){
lang.helloJS();
}else{
lang.helloDeno();
}
- 使用 in ,属性名是否在对象中
enum Type { JavaScript , Python };
class JavaScript {
JS:any;
helloJS(){
console.log("hello JavaScript!");
}
}
class Deno {
Py:any;
helloDeno(){
console.log("hello deno!");
}
}
function getLangage(type:Type):void{
const lang = type === 0 ? new JavaScript() : new Deno();//const lang: JavaScript | Deno
if("JS" in lang){
lang.helloJS();
}else{
lang.helloDeno();
}
console.log(lang);
}
getLangage(Type.JavaScript);
- typeof 检测基本类型的方法
function main(x:string | number){
if(typeof x === "string"){
console.log("x is string");
}else {
console.log("x is number");
}
}
main(12);
- 类型保护函数
要定义一个类型守卫,我们只要简单地定义一个函数,它的返回值是一个类型谓词:
enum Type { JavaScript , Python };
class JavaScript {
JS:any;
helloJS(){
console.log("hello JavaScript!");
}
}
class Deno {
PY:any;
helloDeno(){
console.log("hello JavaScript!");
}
}
function getLangage(type:Type):void{
const lang = type === 0 ? new JavaScript() : new Deno();//const lang: JavaScript | Deno
if(ISJS(lang)){
lang.helloJS();
}else{
lang.helloDeno();
}
console.log(lang);
}
getLangage(Type.JavaScript);
function ISJS(lang : JavaScript | Deno):lang is JavaScript{
return (<JavaScript>lang).helloJS !== undefined;
}
在这个例子里,lang is JavaScript
就是类型谓词。 谓词为 parameterName is Type
这种形式,parameterName` 必须是来自于当前函数签名里的一个参数名。
二、索引类型(Index types)
索引类型其实也不算一个类型,其实就是一个函数,现在我们写出以下代码:
var obj = {
a:1,
b:2,
c:3,
}
function getValues(obj:any,keys:string[]){
return keys.map(key=>obj[key]);
};
console.log(getValues(obj,["a","b"])); //[1,2]
从一个对象里面来获取属性的子集,getValues(obj,["a","b"]
能够得到期望的结果,但是如果我们通过getValues(obj,["d"]
去得到对象属性 d 的值,很遗憾的是 undefined ,这没啥问题,但是别忘了 TypeScript 是强类型语言,照理来讲我们 getValues(obj,["d"]
去调用一个对象里面不存在的属性,应该报错才是符合强类型语言的思路,索引类型就是为了这个目标出现的。
先来了解几个前景知识:
- 索引类型查询操作符:
keyof T
interface Person {
name: string;
age : number;
}
let personProps: keyof Person; // 'name' | 'age'
对于任何类型 T,keyof T
的结果为 T 上已知的公共属性名的联合。keyof Person 是完全可以与 'name' | 'age'互相替换的。 不过不同的是如果你添加了其它的属性到 Person
,例如 address: string,
那么 keyof Person
会自动变为 'name' | 'age' | 'address'
。 你可以在像 getValues 函数这类上下文里使用 keyof,因为在使用之前你并不清楚可能出现的属性名, 但编译器会检查你是否传入了正确的属性名给 getValues。这句话后面解释。
- 索引访问操作符:
T[K]
:
interface Person {
name: string;
age : number;
}
let personProps: keyof Person; // 'name' | 'age'
// 索引访问操作符
let value:Person["name"];//value:string;
T[K]
只不过是拿到对象属性值的类型。
现在来实现,我们要对输入输出进行约束,首先想到的应该是泛型函数,第二个参数又是数组,对应的应该使用泛型数组函数:
var obj: = {
a:1,
b:2,
c:3,
}
function getValues<T,K>(obj:T,keys:K[]):K[]{
return keys.map(key=>obj[key]);//报错,因为数组里面的元素可以是任意类型
};
obj[key]
报错因为 keys ,是个数组结合泛型使用,数组里面的元素会被翻译成任意类型,number 就是其中一种,而对象的属性是不允许为 number 所以报错。这时这句话就有用了:
你可以在像 getValues 函数这类上下文里使用 keyof,因为在使用之前你并不清楚可能出现的属性名,
但编译器会检查你是否传入了正确的属性名给 getValues。这句话后面解释。
这时用到泛型约束(extends)结合索引类型查询操作符:
//K extends keyof T 表示,现在 K 是 `a/b/c/x` 并不是类型了,所以T[K]
function getValues<T,K extends keyof T>(obj:T,keys:K[]):K[]{
return keys.map(key=>obj[key]);//报错返回值类型不对
};
console.log(getValues(obj,["a","b"])); //[1,2]
原因是现在的 K 不是类型,所以用到索引访问操作符:T[K]正确的写法:
function getValues<T,K extends keyof T>(obj:T,keys:K[]):T[K][]{
return keys.map(key=>obj[key]);//报错,因为数组里面的元素可以是任意类型
};
console.log(getValues(obj,["a","b"])); //[1,2]
到此大功告成,在输入对象里面没有出现的属性就会报错:
getValues(obj,["d"]);//Type 'string' is not assignable to type '"a" | "b" | "c"'.ts(2322)
三、映射类型
TypeScript 提供了从旧类型中创建新类型的一种方式 — 映射类型。
例如,定义一个接口 obj:
interface obj {
a : number;
b : string;
c : boolean;
}
现在要求把 obj 的元素变为只读,简单我们只需要:
interface obj {
readonly a : number;
readonly b : string;
readonly c : boolean;
}
这个有点麻烦 TS 为我们提供了映射类型可以方便这个操作,只需要一行代码:
//可以这样使用
var avc:Readonly<obj>;
//推荐这样使用
type ReadonlyObj = Readonly<obj>;
此处一个动图
上面动图就是源码,我们来看看是怎么写出来的,源码摘抄如下:
/**
* Make all properties in T readonly
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
索引类型查询操作符 keyof T
我们知道是干啥的了,现在主要是弄懂 P 代表啥,也就是 in 操作符的使用,来一段代码瞬间看懂:
type Keys = 'name' | 'age';
type Flags = { [K in Keys]: boolean };
const flagObj:Flags= {
name:true,
age:false,
}
它的语法有三个部分:
- 类型变量K,它会依次绑定到联合类型的每个属性。
- 字符串字面量联合的 Keys,它包含了要迭代的属性名的集合。
- 属性的结果类型。
等于号后面的看懂了,在看等于号前面的 type 定义类型别名,为了更方便的使用引入接口泛型。
映射类型常用的有以下几个:
- 必选属性变只读属性,案例见上
- 必选属性变可选属性,案例如下:
interface obj {
a : number;
b : string;
c : boolean;
}
type ParticalObj = Partial<obj>;
/* type ParticalObj = {
a?: number;
b?: string;
c?: boolean;
}
*/
这个源代码是实现就更简单了,只需要加一个?
。
- 抽取不部分必选属性:
interface obj {
a : number;
b : string;
c : boolean;
}
type pickObj = Pick<obj , 'a' | 'b'>;
/* type pickObj = {
a: number;
b: string;
} */
Pick 的实现源码:
/**
* From T, pick a set of properties whose keys are in the union K
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
上面这三个都属于同态,意思是不会作用于新属性,只会作用于指定的 obj 。再来介绍一个非同态:
interface obj {
a : number;
b : string;
c : boolean;
}
type recordObj = Record<'x' | 'y' , obj>;
/* type recordObj = {
x: obj;
y: obj;
} */
上面你可能看懵了,先看看这个例子:
type recordObj = Record<'x' | 'y' , number>;
/* type recordObj = {
x: number;
y: number;
} */
这个完全可以改成 interface 接口来实现。
四、条件类型
有条件类型
TypeScript 2.8 引入了有条件类型,它能够表示非统一的类型.。
官网多个有字,无所谓了,就叫条件类型就行了,条件类型无疑是增加了类型的灵活性,这就是它最大的有点。一个标准的条件类型表达式为:
T extends U ? X : Y
上面的类型意思是,若 T 能够赋值给 U,那么类型是 X,否则为 Y。
偷盗官网示例:
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type T0 = TypeName<string>; // "string"
type T1 = TypeName<"a">; // "string"
type T2 = TypeName<true>; // "boolean"
type T3 = TypeName<() => void>; // "function"
type T4 = TypeName<string[]>; // "object"
看懂上面再来看分布式有条件类型:条件类型里面有联合类型
// (A | B) extends T ? X : Y;可分解为
// (A extends T ? X : Y) | (B extends T ? X : Y);
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
// (A | B) extends T ? X : Y;可分解为
// (A extends T ? X : Y) | (B extends T ? X : Y);
type T5 = TypeName<string | number>;
// type T5 = "string" | "number"
我们现在利用分布式条件类型,做个案例,从类型 T 中去除可以赋值给 U 的类型:
type Diff< T , U > = T extends U ? never : T;
type diff = Diff< 'a' | 'b' | 'c' |'d' , 'a' | 'b'>
猜猜上面代码的结果:
type diff = 'c' | 'd';
没猜对的话我给你分解下真正执行的代码:
('a' extends 'a' | 'b' ? never : 'a') | ('b' extends 'a' | 'b' ? never : 'b') | ('c' extends 'a' | 'b' ? never : 'c') | ('d' extends 'a' | 'b' ? never : 'd')
刚刚上面这个案例,从 T 中剔除可以赋值给 U 的类型,可以直接调用 TS 提供的 API。
type diffoo = Exclude< 'a' | 'b' | 'c' |'d' , 'a' | 'b'>
//Exclude 逻辑实现就是我们上面写的
Exclude 就是传说中的预定义的有条件类型。常见的有,我都不讲常用的:
TypeScript 2.8 在 lib.d.ts 里增加了一些预定义的有条件类型:
- Exclude<T, U> -- 从T中剔除可以赋值给U的类型。
- Extract<T, U> -- 提取T中可以赋值给U的类型。
- NonNullable<T> -- 从T中剔除null和undefined。
- ReturnType<T> -- 获取函数返回值类型。
type fun = ReturnType<()=>string>
- InstanceType<T> -- 获取构造函数类型的实例类型。
这里只讲下 ReturnType<T>
的实现,首先:
现在在有条件类型的 extends 子语句中,允许出现 infer 声明,它会引入一个待推断的类型变量。 这个推断的类型变量可以在有条件类型的 true 分支中被引用。 允许出现多个同类型变量的 infer。
提取函数类型的返回值类型就用到了 infer:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
用到 infer 就叫有条件类型中的类型推断,infer U 就表示不指定 U 的类型,你去类型推断 U 是啥类型,你也可以认为 U 就是 any 类型。