typescript学习-类型操作(二)

85 阅读5分钟

一、条件类型

在代码中,三元表达式是常见的一种语法,如:

let a = 10
let b = a === 10 ? a : 5

在typescript的类型系统中,同样可以使用三元表达式,先看一个简单示例:

interface Animal {
  live(): void;
}
interface Dog extends Animal {
  woof(): void;
}
type Example1 = Dog extends Animal ? number : string; // number 
type Example2 = RegExp extends Animal ? number : string; // string

简单来说,在类型系统中,可以根据一个类型是否满足一个类型约束,从而确定具体的类型。 语法如下:SomeType extends OtherType ? TrueType : FalseType;这个类型三元表达式乍一看好像没什么用,还不如直接进行类型定义来的简单明了,但确实在某些场景下,是极其方便的,如下:

  • 语法简化

    假定存在这么一个场景,有一个方法,接收一个参数,根据参数的类型不同,返回不同的类型。具体定义如下:

    interface IdLabel {
      id: number
    }
    interface NameLabel {
      name: string
    }
    function createLabel(id: number): IdLabel;
    function createLabel(name: string): NameLabel;
    function createLabel(nameOrId: string | number): IdLabel | NameLabel {
      throw "unimplemented";
    }
    let a = createLabel(1) // IdLable
    let b = createLabel('a') // NameLabel
    

    在上述中,采用了重载来解决类型推导的问题。但当我们不断的新增类型时,这个重载会越来越多,这会导致阅读和维护上的问题。因此采用条件类型,可以很简洁的解决这个问题。如下:

    interface IdLabel {
      id: number /* some fields */;
    }
    interface NameLabel {
      name: string /* other fields */;
    }
    // 条件类型
    type NameOrId<T extends number | string> = T extends number
      ? IdLabel
      : NameLabel;
    
    function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
      throw "unimplemented";
    }
    let a = createLabel(1) // IdLable
    let b = createLabel('a') // NameLabel
    

    就是利用泛型的来进行参数类型的约束,根据具体的类型,使用条件类型来确定其返回类型。这样一个条件类型的声明,可以直接替换掉重载的定义。

  • 类型推断

    除了上述的语法简化上的好处,更方便的一个特性在于其使用infer关键词,可以实现自动的类型推导。官方提供了一个类型ReturnType便是基于此实现,下面简单进行分析。

    type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
    
    • 泛型T进行类型约束

      泛型T约束了类型变量必须是一个方法类型。

    • 泛型R进行类型推导

      type A = () => boolean
      type B = ReturnType<A>
      

      我以上述示例进行举例。当传入一个方法类型A时,会进行判断是否满足约束。但在判断中使用infer声明了泛型变量R,此时则无法进行判断,因为R是一个变量,这个约束并不固定,此时会由判断逻辑变更为匹配逻辑,匹配满足的约束。很显然,此时类型A返回类型是boolean,则变量R也必须是boolean才满足,则此时会自动推导出类型boolean,如果没有匹配的约束,则返回false分支的类型any。

    需要注意的是,类型推断使用infer声明的泛型变量必须与ture分支的变量一致。有点类似解一个一元方程:

    //y = a + x
    //y:传入的类型 () => boolean
    //a:参数的约束 () => (...args:any) y的参数没有,是满足约束的
    //x:返回的约束 () => infer R : boolean
    
  • 联合类型

    需要注意,当使用泛型时,如果传入的类型参数是一个联合类型,则新类型一般也是一个联合类型。如下:

    type ToArray<Type> = Type extends any ? Type[] : never;
    type StrArrOrNumArr = ToArray<string | number> // string[] | number[]
    

    在上述中,如果想得到(string | number)[],则需要特殊写法:

    type ToArray<Type> = [Type] extends [any] ? Type[] : never;
    type StrArrOrNumArr = ToArray<string | number> // (string | number)[]
    

二、映射类型

当想根据一个已有类型快速创建另一种类型时,可能会采用的类型映射。简单来说,就是利用inkeyof操作符,快速的将一个已有类型的属性名获取出来。常规用法如下:

type OptionsFlags<Type> = {
  [Property in keyof Type]: Type[Property];
};
  • keyof

    keyof操作符会根据Type类型的属性名生成一个属性名的联合类型

  • in

    遍历keyof操作符生成的联合类型的每一个类型,作为新类型的属性名

1.映射修饰符

当没有任何修饰符的时候,新类型将保持原有类型的修饰符。如下:

type A = {
  a:number,
  b?:boolean,
  readonly c:string
}
type B = {
  [prop in keyof A]:boolean
}
// type B = {
//   a: boolean;
//   b?: boolean | undefined;
//   readonly c: boolean;
// }
  • readonly修饰符

    当想为新类型增减readonly修饰符时,可以采用如下操作:

    type A = {
      a:number,
      b?:boolean,
      readonly c:string
    }
    type B = {
      readonly [prop in keyof A]:boolean
    }
    // type B = {
    //   readonly a: boolean;
    //   readonly b?: boolean | undefined;
    //   readonly c: boolean;
    // }
    // =================================
    type A = {
      a:number,
      b?:boolean,
      readonly c:string
    }
    type B = {
      -readonly [prop in keyof A]:boolean
    }
    // type B = {
    //   a: boolean;
    //   b?: boolean | undefined;
    //   c: boolean;
    // }
    
  • ?修饰符

    当想为新类型增减?修饰符时,可以采用如下操作:

    type A = {
      a:number,
      b?:boolean,
      readonly c:string
    }
    type B = {
      [prop in keyof A]?:boolean
    }
    // type B = {
    //   a?: boolean | undefined;
    //   b?: boolean | undefined;
    //   readonly c?: boolean | undefined;
    // }
    // =====================================
    type A = {
      a:number,
      b?:boolean,
      readonly c:string
    }
    type B = {
      [prop in keyof A]-?:boolean
    }
    // type B = {
    //   a: boolean;
    //   b: boolean;
    //   readonly c: boolean;
    // }
    

2.重命名

映射操作的本质是一个遍历,因此可以在遍历途中进行属性重命名。简单语法如下:

type A = {
  a:string,
  b:number,
  c:boolean
}
type Getters<Type> = {
  [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
  // 遍历每一个属性名时,添加get前缀,并将属性名称大写
};
type B = Getters<A>
// type B = {
//   getA: () => string;
//   getB: () => number;
//   getC: () => boolean;
// }

在上述操作中,主要用到了一个叫做模板类型操作的技术。

  • 移除属性

    可能在某些时候,我们不想要某些属性,此时只需要将这些属性的类型变为never即可。

    type RemoveKindField<Type> = {
      [Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
    };
    interface Circle {
      kind: string;
      radius: number;
    }
    type KindlessCircle = RemoveKindField<Circle>;
    // type KindlessCircle = {
    //   radius: number;
    // }
    

    在类型映射操作时,当Property是kind时,Exclude<Property, "kind">是never,因此kind在新类型中被移除。当然,这种写法不灵活,但只要满足as的对象是一个类型,则语法上都是满足,如下示例:

    type RemoveKindField<Type,E extends keyof Type> = {
      [Property in keyof Type as `get${Capitalize<string & (Property extends E ? never : Property)>}` ]:() => Type[Property]
    };
    
    interface Circle {
      kind: string;
      radius: number;
      long:number
    }
    
    type KindlessCircle = RemoveKindField<Circle,"radius">;
    // type KindlessCircle = {
    //   getKind: () => string;
    //   getLong: () => number;
    // }
    

    上述映射操作部分的语法是很复杂的,但依旧可以正常推断类型,并且这种写法变得十分灵活,根据泛型参数的传入,可以随意移除属性。

三、模板字面量类型

模板字面量类型其实很类似javascript的模板语法,只不过是在typescript的类型系统中使用。简单例子如下:

type World = "world";
type Greeting = `hello ${World}`;

将已有类型放入模板块中,生成一个新的类型。需要注意的是,当模板中是一个联合类型时,那么新类型是所有的可能类型。

type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
 
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
// type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"

除此之外,还有更复杂的场景,同时存在几个类型变量

type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`; // 4
type Lang = "en" | "ja" | "pt"; // 3
 
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`; // 3 * 4 = 12

此时LocaleMessageIDs的类型是12种类型组合而成的联合类型

不仅如此,官方也预定义了几种类型,是编译器自带,无法在d.ts中找到具体定义的。

  • Uppercase 全部大写
  • Lowercase 全部小写
  • Capitalize 首字母大写
  • Uncapitalize 首字母小写
type A = Uppercase<"hello"> // HELLO
type B = Lowercase<"HELLO"> // hello
type C = Capitalize<'hello'> // Hello
type D = Uncapitalize<'HELLO'> // hELLO

需要注意的是,示例中的hello字符串是一个字面量类型,不是值