高级类型

1. 交叉类型

符号: &

如: Person & Serializable & Loggable 返回的类型,同时拥有了这三个类型的成员。

这通常出现在 extendmixins

interface A {
    a: string;
}

interface B {
    b: string;
}

let a = { a:'1' };
let b = {b:'1'};
function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U>{};
    for (let id in first) {
        (<any>result)[id] = (<any>first)[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            (<any>result)[id] = (<any>second)[id];
        }
    }
    return result;
}

extend(a,b)

T & U 即有 a 属性,也有 b 属性。

2. 联合类型

符号: |

表示几种类型之一,如 number | string | boolean 表示一个值可以是 numberstring 或者 boolean类型

如果一个值的类型是联合类型,那么只能访问所有类型中共有的成员:

interface A {
    a: number,
    b: number,
}

interface B {
    b: number,
    c: number,
}

// let a: A | B = { a: 1, b: 2, };
// a.a;  success, 因为 a 中确实有 a 属性

function getSmallPet(): A | B {
    return { a: 2 } as A | B;
}

let pet = getSmallPet();
pet.a // errors
// a 属性在 B 中不存在,所以会报错,毕竟假如返回了B类型,那么就会报错

pet.b // okay

为了使以上代码不报错,我们可以使用类型断言:

let pet = getSmallPet();
(<A>pet).a

如果我们多次用到 pet.a, 那么就要使用多次类型断言

if((<A>pet).a) {
    const c = (<A>pet).a;
}

因此引入了类型保护的概念

3. 类型保护

自定义类型保护

要定义一个类型保护,只要定义一个函数,返回值是一个类型谓词即可:

function isFish(pet: A | B): pet is A {
    return (<A>pet).a !== undefined;
}

pet is A就是类型谓词,谓词为 parameterName is Type, parameterName 必须是来自当前函数签名里的参数名。

这样使用变量调用isFish时,TypeScript会将变量缩减为 is 之后的那个类型,只要该类型与变量的原始类型兼容。

function isFish(pet: A | B): pet is A {
    return (<A>pet).a !== undefined;
}

let a:A | B = {a:1,b:2}

if (isFish(a)) {
    // 如果返回 true,那么 a 就是 A 类型
    console.log(a.a)
} else {
    // 否则就是 B 类型
    console.log(a.c);
}

typeof类型保护

再次回顾之前的 isFish 函数,也就是说通过调用该函数,那么就能确保某个作用于里的类型,如isFish为true,那么

if (isFish(a)) {
    // 如果返回 true,那么 a 就是 A 类型
    console.log(a.a)
}

这段代码里,a就是A类型。

那如果我们要判断某个值是不是原始类型,如 string, number 可以通过 typeof 来判断:

function isString(val: any): val is string{
    return typeof val === 'string';
}

if(isString(params)) {
    // params 即为 string 类型
}

但是,每次原始类型都得写一个isXxx函数来判断,这太过麻烦。好在TypeScript 直接将 typeof x === 'string' 识别为类型保护:

function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        // 这里padding 就是number类型
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
    // 这里padding 就是 string 类型
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

typeof 这种类型保护只能这样使用, typeof v === typename 或者 typeof v !== typename(是指其他判断如 ['type1','type2'].includes(typeof v) 无效吧)。

此外,typename 只能是 number,string,boolean 或者 symbol

instanceof类型保护typeof 类似

if (padder instanceof StringPadder) {
    // 则 padder 就是 StringPadder 类型
    // 拥有 StringPadder 原型的方法
}

4. 可以为 null 的类型

通常 null 和 undefined 可以赋值给任何其他的类型,即使你想阻止也不行。

但是如果你设置 --strictNullChecks,则 null 和 undefined ,除非你使用联合类型包含 null 和 undefined, 否则是不能赋值给其他类型的:

let s = "foo"
s = null;  // 错误,

let s:string | null = "foo"
s = null;  // 正确
s = undefined;  // 错误

let s:string | null | undefined = "foo"
s = null;  // 正确
s = undefined;  // 正确

即这种情况下,string | null, string | undefined和 string | undefined | null 都是不同的类型

可选参数和可选属性

使用 --strictNullChecks, 可选参数会被自动加上 | undefined

function f(x: number, y?: number) {
    return x + (y || 0);
}

f(1);  // ok
f(1, undefined);  //ok 
f(1, null); // error, 'null' is not assignable to 'number | undefined'

同样的可选属性也是如此:

class C {
    a: number;
    b?: number;
}
let c = new C();

c.b = null;    // // error, 'null' is not assignable to 'number | undefined'

类型保护和类型断言

当代码中需要通过类型保护去除 null 时,可以通过以下方式,这和在JS中一样

function f(sn: string | null): string {
    if(sn === null) {
        return 'default'
    } else {
        return sn;
    }
}

或者使用短路运算符
function f(sn: string | null): string {
    return sn || 'default'
}

以上两种情况是编译器去除的 null,但是如果编译器不能处理时(比如存在嵌套函数调用的情况时),需要手动添加类型断言去除:

function broken(name: string | null): string {
  function postfix(epithet: string) {
    return name.charAt(0) + '.  the ' + epithet; // error, 'name' is possibly null
  }
  name = name || "Bob";
  return postfix("great");
}

以上这段代码,尽管我们知道 epither 函数不会是 null,但是编译器不知道,这时候需要手动添加类型断言:语法是添加 ! 后缀

function broken(name: string | null): string {
  function postfix(epithet: string) {
    return name!.charAt(0) + '.  the ' + epithet; // ok
  }
  name = name || "Bob";
  return postfix("great");
}

5. 类型别名

类型别名可以给类型起一个新名字,和接口有点类似,但是可以给原始值,联合类型,元祖以及其他任何需要手写的类型进行起名:

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;

function getName(n: NameOrResolver): Name {
    if(typeof n === 'string') {
        return n;
    } else {
        return n();
    }
}

起别名不是新建类型,只是新建名字来引用那个类型。给原始类型起别名通常没有什么用,只是作为文档时,可阅读性强一些。

同接口一样,类型别名也可以是泛型

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

// 可以在属性中引用类型别名
type Tree<T> = {
    value: T;
    left: Tree<T>;
    right: Tree<T>;
}

接口和类型别名的区别

  • 接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名

  • 类型别名不能被 extends和 implements(自己也不能 extends和 implements其它类型。 因此应该尽量使用接口代替别名

  • 如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名

字符串字面量类型

字符串字面量允许指定字符串必须的固定值,可以与联合类型,类型保护和类型别名很好的配合。结合这些特性,可以实现实现类似枚举的字符串

type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
    animate(easing: Easing) {
        if (easing === "ease-in") {
            // ...
        }
        else if (easing === "ease-out") {
        }
        else if (easing === "ease-in-out") {
        }
        else {
            // error! should not pass null or undefined.
        }
    }
}

let button = new UIElement();
button.animate("ease-in");
button.animate("uneasy"); // 错误,限制了只能使用 "ease-in","ease-out","ease-in-out" 三个之一

字符串字面量类型还可以用于区分函数重载:

function createElement(tagName: "img"): HTMLImageElement;  图片元素
function createElement(tagName: "input"): HTMLInputElement; Input元素
// ... more overloads ...
function createElement(tagName: string): Element {
    // ... code goes here ...
}

数字字面量类型

function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
    // ...
}
一般很少像上面代码一样直接使用,但是可以用在缩小范围 debug 的时候

function foo(x: number) {
    if (x !== 1 || x !== 2) {
        // 报错,因为 1 和 2 没有交集。因此以上代码一定是 true
        // number 不可能 === 1 且  === 2
    }
}

6. 可辨识联合

通过合并,单例类型,类型保护和类型别名来创建一个可辨识联合的高级模式

interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}

以上三个接口的 kind 作为 可辨识的特征,现在可以联合起来:

type Shape = Square | Rectangle | Circle;

使用可辨识联合:

function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

完整性检查

多态的 this类型

class BasicCalculator {
    public constructor(protected value: number = 0) { }
    public currentValue(): number {
        return this.value;
    }
    // 返回 this 类型,
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
    public multiply(operand: number): this {
        this.value *= operand;
        return this;
    }
    // ... other operations go here ...
}

当前 this 为 BasicCalculator 类型,当继承时, this 为子类类型

7. 索引类型

索引类型查询: keyof

索引类型访问: T[K]

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
  return names.map(n => o[n]);
}

interface Person {
    name: string;
    age: number;
}
let person: Person = {
    name: 'Jarid',
    age: 35
};
let strings: string[] = pluck(person, ['name']); // ok, string[]

索引类型和字符串索引签名 keyof和 T[K]与字符串索引签名进行交互。 如果你有一个带有字符串索引签名的类型,那么 keyof T会是 string。 并且 T[string]为索引签名的类型:

interface Map<T> {
    [key: string]: T;
}
let keys: keyof Map<number>; // string 类型,即 key 的类型
let value: Map<number>['foo']; // number, Map<number> 为对象,属性为 foo 时,值为 number 类型

映射类型

即通过映射将类型改变为另一个类型,如将已知类型的每个属性变为可选的,或者变为只读的

原始类型  
interface Person {
    name: string;
    age: number;
}

可选类型 
interface PersonPartial {
    name?: string;
    age?: number;
}

只读类型
interface PersonReadonly {
    readonly name: string;
    readonly age: number;
}

此时我们可以定义映射类型,将类型进行转换

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

使用:

type PersonPartial = Partial<Person>;  // 得到可选类型
type ReadonlyPerson = Readonly<Person>;  // 得到只读类型

更复杂的映射,为每个属性添加代理

type Proxy<T> = {
    get(): T;
    set(value: T): void;
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>;
}
function proxify<T>(o: T): Proxify<T> {
   // ... wrap proxies ...
}
let proxyProps = proxify(props);

Readonly<T>Partial<T> 用处不小,因此它们与 PickRecord 一同被包含进了TypeScript的标准库里:

Pick

// 根据对象上的几个属性,得到一个新的类型
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}

type Flags = {
    option1: boolean;
    option2: boolean;
}

type A  = Pick<Flags,'option1'>
A  {
    option1: boolean;
}

Record

// 根据属性列表,构建一个新的类型
type Record<K extends string, T> = {
    [P in K]: T;
}

type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>

ThreeStringProps为 {
    prop1: string,
    prop2: string,
    prop3: string
}

8. 预定义的有条件类型

  • Exclude -- 从T中剔除可以赋值给U的类型。 T-U

  • Extract -- 提取T中可以赋值给U的类型。 T 与 U 的交集

  • NonNullable -- 从T中剔除null和undefined。

  • ReturnType -- 获取函数返回值类型。

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

type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"

type T02 = Exclude<string | number | (() => void), Function>;  // string | number
type T03 = Extract<string | number | (() => void), Function>;  // () => void

type T04 = NonNullable<string | number | undefined>;  // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined>;  // (() => string) | string[]

function f1(s: string) {
    return { a: 1, b: s };
}

class C {
    x = 0;
    y = 0;
}

type T10 = ReturnType<() => string>;  // string
type T11 = ReturnType<(s: string) => void>;  // void
type T12 = ReturnType<(<T>() => T)>;  // {}
type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>;  // number[]
type T14 = ReturnType<typeof f1>;  // { a: number, b: string }
type T15 = ReturnType<any>;  // any
type T16 = ReturnType<never>;  // any
type T17 = ReturnType<string>;  // Error
type T18 = ReturnType<Function>;  // Error

type T20 = InstanceType<typeof C>;  // C
type T21 = InstanceType<any>;  // any
type T22 = InstanceType<never>;  // any
type T23 = InstanceType<string>;  // Error
type T24 = InstanceType<Function>;  // Error

Last updated