TypeScript真香系列-高级类型

1,202 阅读7分钟

前言

TypeScript真香系列的内容将参考中文文档,但是文中的例子基本不会和文档中的例子重复,对于一些地方也会深入研究。另外,文中一些例子的结果都是在代码没有错误后编译为JavaScript得到的。如果想实际看看TypeScript编译为JavaScript的代码,可以访问TypeScript的在线编译地址,动手操作,印象更加深刻。

交叉类型

交叉类型是将多个类型合并为一个类型,相当于一种并的操作。

interface IDog { 
    name: string,
    age: number,
}
interface ICat {
    name: string,
    color: string
}

let animal: IDog & ICat;
animal = {
    name: "哈士奇",
    age: 1,
    color: "white",
}
animal.name; // "哈士奇"
animal.age;  // 1

上面animal中的属性一个都不能少,如果少了属性的话,就会出现下面的错误: i

nterface IDog { 
    name: string,
    age: number,
}
interface ICat {
    name: string,
    color: string
}

let animal: IDog & ICat;
animal = {   //错误,color属性在ICat中是必须的
    name: "哈士奇",
    age: 1,
    // color: "white",
}

联合类型

联合类型可以说是和交叉类型相反,声明的类型不确定,可以是多个类型中的一个或几个。

let a: number | string;
a = 1;
a = "s";
a = false; // 错误,类型false不能分配给类型 number | string

看一个和交叉类型相对应的例子:

interface IDog { 
    name: string,
    age: number,
}
interface ICat {
    name: string,
    color: string
}

let animal: IDog | ICat; // 这里我们把&改成了|
animal = {     //没有报错
    name: "哈士奇",
    age: 1,
    // color: "white",
}
animal.name;
animal.age;

再看一个例子:

interface IDog { 
    name: string,
    age: number,
}
interface ICat {
    name: string,
    color: string
}

let animal: IDog | ICat;
animal = {
    name: "哈士奇",
    age: 1,
    color: "white",
}
animal.name;
animal.age; //错误,age不存在于ICat. age不存在于IDog | ICat

我们可以看见上面的例子出现了错误,这是因为TypeScript编译器age不知道是IDog还是ICat,所以只能访问公共的name属性。如果我们想要访问这个属性的话,该怎么办?我们可以使用类型断言:

interface IDog { 
    name: string,
    age: number,
}
interface ICat {
    name: string,
    color: string
}

let animal: IDog | ICat;
animal = {
    name: "哈士奇",
    age: 1,
    color: "white",
}
animal.name;
(<IDog>animal).age; // 1

这下就能访问age属性了。

类型保护

有时候我们会遇到类似于下面这种场景:

interface IDog { 
    name: string,
    age: number,
}
interface ICat {
    name: string,
    color: string
}

function animal(arg: IDog | ICat): any {
    if (arg.color) {     //错误
        return arg.color  //错误
    }
}

但是上面的代码会出现错误。如果想要上面这段代码正常工作,可以和联合类型中的例子一样,使用类型断言:

interface IDog { 
    name: string,
    age: number,
}
interface ICat {
    name: string,
    color: string
}

function animal(arg: IDog | ICat):any {
    if ((<ICat>arg).color) {
        return (<ICat>arg).color;
    }
}

除了类型断言,我们还可以利用类型保护来进行判断,常用的类型保护有三种:typeof类型保护,instanceof类型保护和自定义类型保护。

typeof类型保护

function animal(arg: number | string): any {
    if (typeof arg === "string") {
        return arg + "猫";
    }
}

typeof类型保护只有两种形式能被识别: typeof v === "typename"typeof v !== "typename"。"typename"必须是 "number", "string", "boolean"或 "symbol"。

instanceof类型保护

class Dog {
    name: string;
    age: number;
    constructor() { 

    };
}
class Cat {
    name: string;
    color: string;
    constructor() { 

    };
}

let animal: Dog | Cat = new Dog();
if (animal instanceof Dog) {
    animal.name = "dog";
    animal.age = 6;
}
if (animal instanceof Cat) {
    animal.name = "cat";
    animal.color = "white";
}
console.log(animal); //Dog {name: "dog", age: 6}

instanceof的右侧要求是一个构造函数,TypeScript将细化为:

  1. 此构造函数的 prototype属性的类型,如果它的类型不为 any的话;
  2. 构造签名所返回的类型的联合。

自定义类型保护

对于一些复杂的情况,我们可以自定义来进行类型保护:

interface IDog { 
    name: string,
    age: number,
}
interface ICat {
    name: string,
    color: string
}
let animal: IDog | ICat;
animal = {
    name: "哈士奇",
    age: 6,
}
function isDog(arg: IDog | ICat): arg is IDog {

    return arg !== undefined;
    
}
if (isDog(animal)) {
    console.log(animal.age);  //6
}

类型别名

类型别名可以给类型取一个别名。类型别名和接口类似,但又有不同。

type Name = number;
type Types = number | string;
type NAndT = Name & Types;
type MyFunc = () => number;

function animal(arg: Types) { 
    return arg;
}
animal("哈士奇"); //"哈士奇"

类型别名可以作用于原始类型、联合类型、泛型等等。

type Dog<T> = { value: T };

function dog(arg: Dog<string>) {
  return arg;
}

dog({ value: "哈士奇" }); //{value: "哈士奇"}
dog({ value: 1});  //错误,类型number不能分配给string
dog("哈士奇");  //错误,参数“哈士奇”不能分配给类型 Dog<string>

接口和类型别名的区别

区别一:接口可以创建新的名字,而且可以在其它任何地方使用;类型别名不创建新的名字,而是起一个别名。 区别二:类型别名可以进行联合,交叉等操作。 区别三:接口可以被extends和implements以及声明合并等,而类型别名不可以。

这里介绍一下声明合并

“声明合并”是指编译器将针对同一个名字的两个独立声明合并为单一声明。 合并后的声明同时拥有原先两个声明的特性。 任何数量的声明都可被合并;不局限于两个声明。

举个例子:

interface IDog {
    name: string;
    setName(arg:string): string;
}

interface IDog { 
    age: number;
}

interface IDog { 
    color: string;
}
let dog: IDog;
dog = {
    color: "black",
    age: 6,
    name: "哈士奇",
    setName: (arg) => { 
        return arg
    }
}

合并之后:

interface IDog { 
    color: string;
    age: number;
    name: string;
    setName(arg:string): string;
}

我们可以看出,后面的接口在合并后出现在了靠前的位置。

字符串字面量类型和数字字面量类型

字符串字面量允许我们指定字符串为必须的固定值。

type Dog = "哈士奇" | "泰迪" | "中华田园犬" | "萨摩耶";

function dog(arg: Dog):any { 
  switch (arg) {
    case "哈士奇":
      return "傻狗";
    case "泰迪":
      return "精力旺盛";
    case "中华田园犬":
      return "忠诚";
    case "萨摩耶":
      return "微笑天使";
  }
}
dog("哈士奇");  //"傻狗"
dog("柯基");  //错误,参数"柯基"不能分配给类型Dog

数字字面量同理。

type Num = 1 | 2 | 3 | 4 | 5 | 6 | 7;

function week(arg: Num):any {
  switch (arg) {
    case 1:
      return "星期一";
    case 2:
      return "星期二";
    case 3:
      return "星期三";
    case 4:
      return "星期四";
    case 5:
      return "星期五";
    case 6:
      return "星期六";
    case 7:
      return "星期日";
  }
}
week(6);  //"星期六"
week(8);  //错误

可辨识联合

我们可以合并单例类型、联合类型、类型保护和类型别名来创建一个叫做可辨识联合的高级模式。它具有三个要素:

  1. 有普通的单例类型属性— 可辨识的特征。
  2. 一个类型别名包含了那些类型的联合— 联合。
  3. 此属性上的类型保护。

可以看看下面这个例子就能理解了:

//我们首先声明了将要联合的接口,目前各个接口之间是没有联系的,
//只是都有一个kind的属性(可以称为可辨识特征或标签)但是有不同的字符串字面量类型
interface IColor {
  kind: "color";
  value: string;
}

interface ISize {
  kind: "size";
  height: number;
  width: number;
}

//然后我们利用类型别名和联合类型把两个接口联合到一起
type MyType = IColor | ISize;

//最后使用可辨识联合
function types(arg: MyType) :any{
  switch (arg.kind) {
    case "color":
      return arg.value;
    case "size":
      return arg.height * arg.width;
  }
}
types({ kind: "color", value: "blue" });  //"blue"
types({ kind: "size", height: 10, width: 20 }); //200

索引类型

使用索引类型,编译器就能够检查使用动态属性名的代码。我们首先要知道两个操作符。

  1. 索引类型的查询操作符 keyof T,意为:对于任何类型Tkeyof T的结果为T上已知公共属性的联合;
  2. 索引访问操作符T[K]
interface IDog{
  a: string,
  b: number,
  c: boolean,
}

let dog: keyof IDog; //let dog: "a" | "b" | "c"
let arg: IDog["a"];  //let arg: string

再看一个较为复杂的例子:

interface IDog{
  name: string,
  age: number,
  value: string,
}

let dog: IDog;
dog = {
  name: "二哈",
  age: 6,
  value: "奥里给"
}

function myDog<T, K extends keyof T>(x: T, args: K[]): T[K][] { 
  return args.map(i => x[i]);
}

myDog(dog, ['name']);  //["二哈"]
myDog(dog, ['name', 'value']);  //["二哈", "奥里给"]
myDog(dog, ['key']);  //错误,类型不匹配

映射类型

有时候我们可以会遇到这种情况,把每个成员都变为可选或者只读:

interface Person{
    name: string;
    agent: number;
}

interface PersonPartial {
    name?: string;
    age?: number;
}

interface PersonReadonly {
    readonly name: string;
    readonly age: number;
}

这在JavaScript里经常出现,TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。

interface IPerson{
    name: string;
    agent: number;
}

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

type OReadonly<T> = {
    readonly [P in keyof T]: T[P];
}
//使用方式
type PersonPartial = OPartial<IPerson>;
type ReadonlyPerson = OReadonly<IPerson>;

参考

github.com/zhongsp/Typ…

github.com/jkchao/type…

最后

文中有些地方可能会加入一些自己的理解,若有不准确或错误的地方,欢迎指出~