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();
}

现在没问题了,但是每个地方我们都加了类型断言,很明显这种方案不是我们想要的可读性差写起来也是很麻烦类型守卫就是解决这个问题的。可以简单理解为类型保护就是解决类型断言可读性差和书写不方便的。

类型保护:能够在特定的区块中(比如函数里)保证变量属于某种特定的类型,可以在区块中放心的使用此类型的属性或方法。

下面介绍四种,还使用上面的案例,只改动条件判断语句:

  1. instanceof
if(lang instanceof JavaScript){
    lang.helloJS();
}else{
    lang.helloDeno();
}
  1. 使用 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);
  1. typeof 检测基本类型的方法
function main(x:string | number){
    if(typeof x === "string"){
        console.log("x is string");
    }else {
        console.log("x is number");
    }
}
main(12);
  1. 类型保护函数
    要定义一个类型守卫,我们只要简单地定义一个函数,它的返回值是一个类型谓词:
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 提供了从旧类型中创建新类型的一种方式 — 映射类型(一般用到 in 遍历枚举类型的地方就是映射类型出现的地方)。

例如,定义一个接口 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];
};

上面这三个都属于同态映射类型(homomorphic mapped types),意思是不会对原类型接口的结构进行改变,对应的非同态就是改变了原结构。再来介绍一个非同态:

interface obj {
    a : number;
    b : string;
    c : boolean;
}

type recordObj = Record<'x' | 'y' , obj>;
/* type recordObj = {
    x: obj;
    y: obj;
} */

得到的类型别名 recordObj 和 obj 的结构已经完全不一样的,这个就叫非同太映射类型。

我们已经知道了 同态类型(homomorphic types) 和 非同态类型(Non-homomorphic types)的区别就是结构不同。但是同态类型(homomorphic types)还有些特性,我们来仔细看看:

interface People {
    readonly name?: string;
    readonly age?: number;
};
type inherit<T> = {
    [ P in keyof T ]: T[P];
};

type legend = inherit<People>;
/*
    legend
    type legend = {
        readonly name?: string | undefined;
        readonly age?: number | undefined;
    }
*/

看到没,同态类型会完整的继承原类型结构上面的 readonly?:,这是它的一大特点。但是如果我们想打破同态这个特性怎么办呢?

第一个想到的就是,工具类型中的 Required Utility Types requiredtype。它就能把可选变为必须,我们来看看它的源代码:

type Required<T> = { [P in keyof T]-?: T[P]; }

相信你也看到了,问好前面多了一个减号,猜你也猜出来它的作用了。它的教程在这:

typescript-2-8 improved-control-over-mapped-type-modifiers

用法很简单,通过加减号来控制是否添加删除 readonly?

-readonly   // Remove readonly
-?          // Remove ?

+readonly   // Add readonly
+?          // Add ?

看例子一个添加和删除 readonly? 的例子:

// add readonly and ?
interface addPeople {
    name: string;
    age: number;
};
type add<T> = {
    +readonly [ P in keyof T ]+?: T[P];
};
type cloneAddPeople = add<addPeople>;
// type cloneAddPeople = {
//     readonly name?: string | undefined;
//     readonly age?: number | undefined;
// }

// remove readonly and ?
interface removePeople {
    readonly name?: string;
    readonly age?: number;
};

type remove<T> = {
    -readonly [ P in keyof T ]-?: T[P];
};

type cloneRemovePeople = remove<removePeople>;
// type cloneRemovePeople = {
//     name: string;
//     age: number;
// }

最后,来看下使用 Record 的结果:

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 里增加了一些预定义的有条件类型:

  1. Exclude<T, U> -- 从T中剔除可以赋值给U的类型。
  2. Extract<T, U> -- 提取T中可以赋值给U的类型。
  3. NonNullable<T> -- 从T中剔除null和undefined。
  4. ReturnType<T> -- 获取函数返回值类型。type fun = ReturnType<()=>string>
  5. 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 类型。

五、综合练习

把一个接口上的所有属性都变得可选。

// 一个用户接口
interface User {
   name: string
   age: number
   contry: string
   friend: {
       name: string
       sex: string
   }
}

type CusPartial<T> = {
    [ P in keyof T ]+?: T[P] extends object ? CusPartial<T[P]> : T[P];
};

type userPartical = CusPartial<User>;

这个例子的核心代码中,我们分别用到了,「索引类型」、「映射类型」、「条件类型」和递归,一个特别好的例子。

[ P in keyof T ]+?: T[P] extends object ? CusPartial<T[P]> : T[P];
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,069评论 4 358
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,212评论 1 287
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 105,912评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,424评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,741评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,194评论 1 206
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,553评论 2 307
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,289评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,923评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,251评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,775评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,144评论 2 249
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,698评论 3 228
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,936评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,658评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,214评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,159评论 2 258

推荐阅读更多精彩内容