快速入门TypeScript类型别名

916 阅读3分钟

TypeScript 提供了很多强大的类型别名,通过这些类型别名,可以方便将一种类型转换为另一种类型,本文主要作为常用类型别名的速查及基本的使用。 学习常用的类型别名之前,先来看一些TypeScript中的关键字。

TypeScript中的关键字

extends

T extends U ? X : Y

很明显,它类似于 js 中的三目运算符。事实上,确实可以理解为三目运算符 。若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";

extends 的使用时,如果 T 为联合类型,则会起来类似于 for 循环分解参数的作用,如果 T 的类型为 A | B | C,会被解析为(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y) 。因此可以方便实现过滤联合类型,官网的例子如下:

type Diff<T, U> = T extends U ? never : T;  // Remove types from T that are assignable to U
type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"

type Filter<T, U> = T extends U ? T : never;  // Remove types from T that are not assignable to U
type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"

keyof

keyof 关键字用来返回某种类型的所有键,其结果为联合类型

interface Person {
    name: string,
    age: number
}
type K1 = keyof Person // "name" | "age"
type K2 = keyof any[];  //  number | "length" | "toString" | "toLocaleString" | "pop" | "push" | "concat" | "join" | "reverse" | "shift" | "slice" | "sort" | "splice" | "unshift" | "indexOf" | "lastIndexOf" | ... 13 more ... | "values"

type Mapish = { [k: string]: boolean };
type M = keyof Mapish; // string | number   此处返回 string | number 的原因是 JavaScript 中始终强制将对象的键转换为字符串,因此 obj[0] 和 obj['0'] 始终一致

class Person1 {
    name = 'xiaoping'
    age: number
    constructor(age: number) {
        this.age = age
    }
    say() {
        console.log(`my name is ${this.name}`)
    }
}
type P = keyof Person1 // "name" | "age" | "say"

typeof

JavaScripttypeof 是一个可以在表达式中使用的运算符。在 TypeScript 中的 typeof 可以用来获取一个变量和属性的类型。

let s = "hello";
let n: typeof s; // string

let person = {
    name: 'xiaoping',
    age: 19,
    address: {
        province: "上海"
    }
}
type P = typeof person
/* 相当于
  type P = {
      name: string;
      age: number;
      address: {
          province: string;
      };
  }
*/

function userInfo(age) {
    return {
        name: 'xiaoping',
        age: age
    }
}
type Fn = typeof userInfo
// type Fn = (age: any) => { name: string; age: any; }

infer

infer 用来代表一个待推断的类型变量。这个关键字比较难理解,我们直接来看例子。

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

const add = (x:number, y:number) => x + y
type t = ReturnType<typeof add> // type t = number

ReturnType 代表如果 T 可以赋值给 (...args: any[]) => infer R ,则返回 R 否则返回 any(...args: any[]) => infer R 代表的是参数是任意值,返回值是inter R 的函数。 很明显 add 是符合的,且add 函数的返回值是 number 。因此infer Rnumber 类型。 type t = number inter 不仅仅用来推断返回值,它最强大的功能在于拆包。官网的例子如下:

type Unpacked<T> =
    T extends (infer U)[] ? U :
    T extends (...args: any[]) => infer U ? U :
    T extends Promise<infer U> ? U :
    T;

    此处看起来比较难以理解,我们拆开查看:

type Unpacked<T> = T extends (infer U)[] ? U : T
type S1 = string[]
type T1 = Unpacked<S1>// string

如果 T 可以赋值给 某个类型 (infer U) 的数组 (infer U)[] 则返回 U ,否则返回 T。因此可以很方便的拆包获得 T1string。 同理 Promise 的拆包,也可以很轻松的实现。

type Unpacked<T> = T extends Promise<infer U> ? U : T
type T1 = Unpacked<Promise<{ name: string, age: number }>> //{ name: string; age: number; }

unknown

TypeScript 中,当我们不确定一个类型是什么类型的,可以选择给其声明为any或者unkown。但实际上,TypeScript 推荐使用unknown,因为unknown是类型安全的。如果是any,你可以任意的取值赋值,不会进行任何的类型检查。但unkown就不一样了,必须先进行断言 ,就是使用typeofinstanceof 来判断类型。之后才能进行操作。

is

is 关键字一般用于函数返回值类型中,判断参数是否属于某一类型,并根据结果返回对应的布尔类型

function isString(s: unknown): boolean {
    return typeof s === 'string'
}
function toUpperCase(x: unknown) {
    if (isString(x)) {
        x.toUpperCase() // Error, Object is of type 'unknown'
    }
}

上面的例子虽然已经判断了 sstring 类型时才会调用 toUpperCase 。但是依然会报错,这是由于嵌套函数使 TypeScript 不能做出正确的推断。这时就可以使用 is 关键字。

function isString(s: unknown): s is string {
    return typeof s === 'string'
}

function toUpperCase(x: unknown) {
    if (isString(x)) {
        x.toUpperCase()  // 此时不在报错,正常通过校验
    }
}

强大的内置类型别名

TypeScript 提供了一些强大的类型别名来实现常见的类型转换。

Partial<T>

Partial 可以将所有的属性变为可选类型。

interface Todo {
    title: string,
    description: string;
}
type PartialTodo = Partial<Todo>
/* 相当于
interface PartialTodo {
    title?: string;
    description?: string;
}
*/

源码如下:

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

Required<T>

RequiredPartial 的作用相反,可以将所有可选类型变为必须。

interface Props {
    a?: number;
    b?: string;
}
type RequireProps = Required<Props>
/* 相当于
interface RequireProps {
    a: number;
    b: string;
}*/

源码如下:

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

Readonly

将所有的属性处理为 Readonly

interface Todo {
    title: string,
    description: string;
}
type ReadonlyTodo = Readonly<Todo>
/* 相当于
interface ReadonlyTodo {
    readonly title: string,
    readonly description: string;
}
*/

源码如下:

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

Record<K,T>

构造一个对象类型,其属性为为 K 类型 ,属性值为 T 类型。常用于将一种类型属性映射为另一种类型。

interface CatInfo {
    age: number;
    breed: string;
}
type CatName = "miffy" | "boris" | "mordred";
type Cats = Record<CatName, CatInfo> // { miffy: CatInfo; boris: CatInfo; mordred: CatInfo; }
const cats: Cats = {
    miffy: { age: 10, breed: "Persian" },
    boris: { age: 5, breed: "Maine Coon" },
    mordred: { age: 16, breed: "British Shorthair" },
}

源码如下:

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

Pick<T, K>

通过从 T 中选取一组属性 K 来构造一个新的类型

interface Todo {
    title: string;
    description: string;
    completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">; //
/* 相当于
interface TodoPreview {
    title: string;
    completed: boolean;
}*/

源码如下:

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

Exclude<T, E>

T 中所有可以分配给 E 的类型排除掉之后生成一个新的类型。

type T0 = Exclude<"a" | "b" | "c", "a">;
// 相当于 type T0 = "b" | "c"

type T1 = Exclude<"a" | "b" | "c", "a" | "b">;
// 相当于 type T1 = "c"

type T2 = Exclude<string | number | (() => void), Function>;
// 相当于 type T2 = string | number

源码如下:

type Exclude<T, U> = T extends U ? never : T;

Omit<T, K>

T 中剔除一些属性 K 来生成新的类型

interface Todo {
  title: string;
  description: string;
  completed: boolean;
  createdAt: number;
}
type TodoPreview = Omit<Todo, "description" | "completed">;
const todo: TodoPreview = {
  title: '',
  createdAt: Date.now()
}

源码如下:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Extract<T, U>

T 中所以可以分配给 U 的类型提取出来,生成一个新的类型。和 Exclude 相反

type T0 = Extract<"a" | "b" | "c", "a" | "f">;
// 相当于 type T0 = "a"

type T1 = Extract<string | number | (() => void), Function>;
// 相当于 type T1 = () => void

源码如下:

type Extract<T, U> = T extends U ? T : never;

NonNullable<T>

T 中的 nullundefined 排除掉。

type T0 = NonNullable<string | number | undefined>;
// 相当于 type T0 = string | number

type T1 = NonNullable<string[] | null | undefined>;
// 相当于 type T1 = string[]

Parameters<T>

返回函数类型 T 的参数来构造成一个元组。

declare function f1(arg: { a: number; b: string }): void;

type T0 = Parameters<() => string>;
// type T0 = []

type T1 = Parameters<(s: string) => void>;
// type T1 = [s: string]

type T2 = Parameters<<T>(arg: T) => T>;
// type T2 = [arg: unknown]

type T3 = Parameters<typeof f1>;
// type T3 = [arg: {
//     a: number;
//     b: string;
// }]

type T4 = Parameters<any>;
// type T4 = unknown[]

type T5 = Parameters<never>;
// type T5 = never

源码如下:

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

ConstructorParameters<T>

生成一个由构造函数的参数构成的元组或者数组。如果 T 不是构造函数,返回 nerver

class Fn {
    constructor(name: string, age: number) {

    }
}
type A = ConstructorParameters<typeof Fn> // [name:string,age:number]
                               
type T0 = ConstructorParameters<ErrorConstructor>; //[message?: string]
type T1 = ConstructorParameters<FunctionConstructor>; //string[]
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;

ReturnType<T>

生成一个由函数 T 的返回值构成的类型。配合 typeof 获取函数的返回值类型

type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; // void
type T2 = ReturnType<<T>() => T>;//unknown
type T3 = ReturnType<<T extends U, U extends number[]>() => T>;//number[]
function fn(){
    return{
        name:'xiaoping',
        age:18
    }
}
type T4 = ReturnType<typeof fn>;
// type T4 = {
//     name: string;
//     age: number;
// }
type T5 = ReturnType<any>; // any
type T6 = ReturnType<never>; // never

源码如下:

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

InstanceType<T>

获取构造函数的实例类型。

class C {
    x = 0;
    y = 0;
}
type T20 = InstanceType<typeof C>;  // C
type T21 = InstanceType<any>;  // any
type T22 = InstanceType<never>;  // any

源码如下:

type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;

ThisParameterType<T>

看这个类型别名之前,先来看一下 JavaScript 中怪异的 this 问题。

window.name = "hello world";
let person = {
  name: "xiaoping",
  say: function () {
    console.log(`my name is ${this.name}`);
  }
};

person.say(); //my name is xiaoping
const say = person.say;
say(); //my name is hello world

由于 JavaScript 中的 this 永远指向的是 js 执行栈的栈顶,因此导致当我们用一个变量将 say 函数保存下来的时候,就会使得当前的 this 出现错误,获得非预期的结果。TypeScript 中就增加了一个 this 的参数限定。当函数执行的 this 不符合预期时抛出错误,如下:

interface Person {
    name: string,
    say: (this: Person) => void
}
const person: Person = {
    name: 'xiaoping',
    say: function () {
        console.log(`my name is ${this.name}`)
    }
}
person.say()
const say = person.say
// say()  // // error The 'this' context of type 'void' is not assignable to method's 'this' of type 'Person'.

ThisParameterType 就是用来获取函数的 this

function toHex(this: number) {
  return this.toString(16);
}
type ToHexThis = ThisParameterType<typeof toHex> // number
                            
function fn(value: string) {
    console.log(value)
}
type A = ThisParameterType<typeof fn> //unknown

源码如下:

type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any ? U : unknown;

OmitThisParameter<T>

如果一个函数被限定了 this ,返回一个剔除了 this 之后的函数类型。

function toHex(this: number) {
    return this.toString(16);
}

type ToHexThis = ThisParameterType<typeof toHex> // number
type T1 = OmitThisParameter<typeof toHex> // () => string
type T2 = typeof toHex //  (this: number) => string

const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5);
console.log(fiveToHex());

源码如下:

type OmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T;

ThisType<T>

它不返回任何转换过的类型,而是作为对象字面量上下文 this 的标识,并且要使用这个类型,需要启用配置 -noImplicitThis 。 官方例子如下:可以看出只是为了增加 this 的标识。

type ObjectDescriptor<D, M> = {
  data?: D;
  methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M
};

function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
  let data: object = desc.data || {};
  let methods: object = desc.methods || {};
  return { ...data, ...methods } as D & M;
}

let obj = makeObject({
  data: { x: 0, y: 0 },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx; // Strongly typed this
      this.y += dy; // Strongly typed this
    },
  },
});

obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);

源码的实现也只是简单的定义了一个空接口。

interface ThisType<T> { }

内置的字符串操作类型

  • Uppercase<S>
  • Lowercase<S>
  • Capitalize<S>
  • Uncapitalize<S>

为了严格的限定字符串的操作,TypeScript 内置了一些字符串类型,对字符串进行限定。

type Cases<T extends string> = `${Uppercase<T>} ${Lowercase<T>} ${Capitalize<T>} ${Uncapitalize<T>}`;
type T11 = Cases<'bar'>; // 'BAR bar Bar bar'

TypeScript中实用却容易忽略的方法

常量枚举

TypeScript 中,枚举会被编译成自执行函数,如下:

enum Direction {
  Up = 1,
  Down,
  Left,
  Right,
}
"use strict";
var Direction;
(function (Direction) {
    Direction[Direction["Up"] = 1] = "Up";
    Direction[Direction["Down"] = 2] = "Down";
    Direction[Direction["Left"] = 3] = "Left";
    Direction[Direction["Right"] = 4] = "Right";
})(Direction || (Direction = {}));

这会增加大量的冗余代码,并且在获取值时,会增加一次值的获取过程。 常量枚举,会在编译时被完全删除,从而避免增加额外的开销。如下:

 const enum Direction {
  Up = 1,
  Down,
  Left,
  Right,
}
let a = Direction.Up
"use strict";
let a = 1 /* Up */;

合并类和接口

通过接口声明来扩展类的实例类型. 类构造函数对象不会被修改

declare class Foo {
    public x : number;
}

interface Foo {
    y : string;
}

function bar(foo : Foo)  {
    foo.x = 1; // 没问题, 在类 Foo 中有声明
    foo.y = "1"; // 没问题, 在接口 Foo 中有声明
}

增强现有的模块

通过 declare module "foo" { } 对现有的模块进行增强。官网例子如下:

/ observable.ts
export class Observable<T> {
    // ...
}

// map.ts
import { Observable } from "./observable";
// 扩充 "./observable"
declare module "./observable" {

    // 使用接口合并扩充 'Observable' 类的定义
    interface Observable<T> {
        map<U>(proj: (el: T) => U): Observable<U>;
    }

}
Observable.prototype.map = /*...*/;

// consumer.ts
import { Observable } from "./observable";
import "./map";

let o: Observable<number>;
o.map(x => x.toFixed());

同样的,也可以通过 declare global 声明从模块中扩展全局范围

// Ensure this is treated as a module.
export {};
declare global {
  interface Array<T> {
    mapToNumbers(): number[];
  }
}
Array.prototype.mapToNumbers = function () {
  /* ... */
};

后期学习过程中,继续补充。

参考

TypeScript Utility Types