TypeScript笔记-高级类型

172 阅读5分钟

系列文章

交叉类型(Intersection Types)

交叉类型是将多个类型合并为一个类型,包含这些类型的所有成员。比如A & B & C,则包含A,B,C这三种类型的成员。

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

class Person {
    constructor (public name: string) {}
}

interface Loggable {
    log(): void;
}

class ConsoleLogger implements Loggable {
    log() {}
}

const jim = extend(new Person('Jim'), new ConsoleLogger());
const n = jim.name;
jim.log();

联合类型(Union Types)

指定一个属性既可以传string又可以传number类型的值。

interface Style {
    padding: number | string; // 既可以number又可以string
}
// any会导致传入的值不止限于string和number,可以是其他任意类型,因此不符合需求
interface Style {
    padding: any; 
}

高级使用

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function getSmallPet(): Fish | Bird {

}

let pet = getSmallPet();

如何判断当前pet为Fish还是Bird?如何检查pet是否含有fly或swim方法?

// 错误的方式-每一个成员访问都会报错
if (pet.swim) {
    pet.swim();
} else if (pet.fly) {
    pet.fly();
}

// 推荐的方式,使用断言
if ((<Fish>pet).swim) {
    (<Fish>pet).swim();
} else {
    (<Bird>pet).fly();
}

类型保护

TypeScript里的类型保护机制——类型保护就是一些表达式,他们会在运行时检查以确保在某个作用域里的类型。

如何定义一个类型保护?

只要简单的定义一个函数,他的返回值是一个类型谓词。

// 类型谓词:parameterName is Type的形式,如本例中的 pet is Fish
function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

typeof类型保护

function isNumber(x: any): x is number {
    return typeof x === "number";
}

function isString(x: any): x is string {
    return typeof x === "string";
}

function padLeft(value: string, padding: string | number) {
    if (isNumber(padding)) {
        return Array(padding + 1).join(" ") + value;
    }
    if (isString(padding)) {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}
// 等同于
// 不必将 typeof x === "number"抽象成一个函数,因为TypeScript可以将它识别为一个类型保护。 也就是说我们可以直接在代码里检查类型了。
function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

instanceof类型保护

要求:

  • 此构造函数的prototype属性的类型,如果他的类型不为any
  • 构造签名所返回的类型的联合

字符串字面量类型

字符串字面量类型允许你指定字符串必须的固定值。

// 通过type设置类型别名
type Easing = 'ease-in' | 'ease-out' | 'ease-in-out';
class UIElement {
    animate(dx: number, dy: number, 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(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here

你只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误。

常用的有 HTMLImageElement、HTMLInputElement、Element等。

枚举成员类型

可辨识联合(Discriminated Unions)

你可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合的高级模式,它也称做 标签联合或 代数数据类型。 可辨识联合在函数式编程很有用处。 一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。 它具有3个要素:

  1. 具有普通的单例类型属性— 可辨识的特征。
  2. 一个类型别名包含了那些类型的联合— 联合。
  3. 此属性上的类型保护。
// 此例中type属性作为可辨识属性
interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}
// 可辨识联合
type Shape = Square | Rectangle | Circle;
function area(s: Square) {
    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;
        // 完整性检查,不符合square,rectangle,circle类型,将报错
        default: return assertNever(s);
    }
}
function assertNever(x: never): never {
    throw new Error('unexpected object');
}

多态的this类型

多态的 this类型表示的是某个包含类或接口的 子类型。 这被称做 F-bounded多态性。 它能很容易的表现连贯接口间的继承(链式调用)。

索引类型(Index types)

使用索引类型,编译器就能够检查是用来动态属性名的代码。

// 设置两个类型变量T、K
// T为传入的对象
// K为T的属性名称,如Person的name属性
// return T[K][], 返回对象属性值组成的数组
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
    return names.map(n=> o[n])
}

// K extends keyof T
// 等同于
// type K = 'name' | 'age'; (联合类型)

interface Person {
    name: string;
    age: number;
}
let person: Person = {
    name: 'Jarid',
    age: 35
}
let strings: string[] = pluck(person, ['name']);
// strings = ['Jarid', 35]
  • 索引类型查询操作符:keyof T (对于任何类型 T, keyof T的结果为 T上已知的公共属性名的联合)
  • 索引访问操作符:T[K] (在上例中,T[K]等于Person['name'],因此为string类型)

映射类型

  • 相当于是一个批量操作,比如,将一个已知的类型每个属性都变为可选的(或者readonly)
  • 映射类型转换是同态的
// 原始类型
interface Person {
    name: string;
    age: number;
}

// 期望结果
interface Person {
    name?: string;
    age?: number;
}

// 映射类型
// 属性全部映射为可选的
type Partial<T> = {
    [P in keyof T]? T[P];
    // [P in keyof T]:提取T中的属性
    // T[P]:T类型中P属性对应的类型值
}
// 属性全部映射为readonly
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}

// 使用
type PersonPartial = Partial<Person>;
type PersonReadOnly = Readonly<Person>;

最简单的映射类型

type Keys = 'option1': 'option2';
type Flags = {
    [K in Keys]: boolean;
    // 内部使用了for...in循环获取属性key
}
// 等同于
type Flags {
    'option1': boolean;
    'option2': boolean;
}

同态与非同态

  • 同态——输入类型来拷贝属性,如Readonly,Partial,Pick
  • 非同态——不需要输入类型来拷贝属性,如Record

利用映射,包装与拆包

// 包装
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> {
    
}
let proxyProps = proxify(props);

// 拆包
function unproxify<T>(t: Proxify<T>): T {
    let result = {} as T;
    for (const k in t) {
        result[k] = t[k].get();
    }
    return result;
}
let originalProps = unproxify(proxyProps);