typescript高级类型4

多态的this类型
多态的this类型表示的是某个包含类或接口的子类型。 这被称做F-bounded多态性。 它能很容易的表现连贯接口间的继承,比如。 在计算器的例子里,在每个操作之后都返回this类型:

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

let v = new BasicCalculator(2)
            .multiply(5)
            .add(1)
            .currentValue();

由于这个类使用了this类型,你可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变。

class ScientificCalculator extends BasicCalculator {
    public constructor(value = 0) {
        super(value);
    }
    public sin() {
        this.value = Math.sin(this.value);
        return this;
    }
    // ... other operations go here ...
}

let v = new ScientificCalculator(2)
        .multiply(5)
        .sin()
        .add(1)
        .currentValue();

如果没有this类型,ScientificCalculator就不能够在继承BasicCalculator的同时还保持接口的连贯性。 multiply将会返回BasicCalculator,它并没有sin方法。 然而,使用this类型,multiply会返回this,在这里就是ScientificCalculator。

索引类型(Index types)
使用索引类型,编译器就能够检查使用了动态属性名的代码。 例如,一个常见的JavaScript模式是从对象中选取属性的子集。

function pluck(o, names) {
    return names.map(n => o[n]);
}

下面是如何在TypeScript里使用此函数,通过索引类型查询和索引访问操作符:

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[]

编译器会检查name是否真的是Person的一个属性。 本例还引入了几个新的类型操作符。 首先是keyof T,索引类型查询操作符。 对于任何类型T,keyof T的结果为T上已知的公共属性名的联合。 例如:

let personProps: keyof Person; // 'name' | 'age'

keyof Person是完全可以与’name’ | ‘age’互相替换的。 不同的是如果你添加了其它的属性到Person,例如address: string,那么keyof Person会自动变为’name’ | ‘age’ | ‘address’。 你可以在像pluck函数这类上下文里使用keyof,因为在使用之前你并不清楚可能出现的属性名。 但编译器会检查你是否传入了正确的属性名给pluck:

pluck(person, ['age', 'unknown']); // error, 'unknown' is not in 'name' | 'age'

第二个操作符是T[K],索引访问操作符。 在这里,类型语法反映了表达式语法。 这意味着person[‘name’]具有类型Person[‘name’] — 在我们的例子里则为string类型。 然而,就像索引类型查询一样,你可以在普通的上下文里使用T[K],这正是它的强大所在。 你只要确保类型变量K extends keyof T就可以了。 例如下面getProperty函数的例子:

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
    return o[name]; // o[name] is of type T[K]
}

getProperty里的o: T和name: K,意味着o[name]: T[K]。 当你返回T[K]的结果,编译器会实例化键的真实类型,因此getProperty的返回值类型会随着你需要的属性改变。

let name: string = getProperty(person, 'name');
let age: number = getProperty(person, 'age');
let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'

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

interface Map<T> {
    [key: string]: T;
}
let keys: keyof Map<number>; // string
let value: Map<number>['foo']; // number

映射类型
一个常见的任务是将一个已知的类型每个属性都变为可选的:

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

或者我们想要一个只读版本:

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

这在JavaScript里经常出现,TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。 例如,你可以令每个属性成为readonly类型或可选的。 下面是一些例子:

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 Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };

在真正的应用里,可能不同于上面的Readonly或Partial。 它们会基于一些已存在的类型,且按照一定的方式转换字段。 这就是keyof和索引访问类型要做的事情:

type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }

但它更有用的地方是可以有一些通用版本。

type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P in keyof T]?: T[P] }

在这些例子里,属性列表是keyof T且结果类型是T[P]的变体。 这是使用通用映射类型的一个好模版。 因为这类转换是同态的,映射只作用于T的属性而没有其它的。 编译器知道在添加任何新属性之前可以拷贝所有存在的属性修饰符。 例如,假设Person.name是只读的,那么Partial.name也将是只读的且为可选的。

下面是另一个例子,T[P]被包装在Proxy类里:

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和Partial用处不小,因此它们与Pick和Record一同被包含进了TypeScript的标准库里:

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

Readonly,Partial和Pick是同态的,但Record不是。 因为Record并不需要输入类型来拷贝属性,所以它不属于同态:

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

非同态类型本质上会创建新的属性,因此它们不会从它处拷贝属性修饰符。

由映射类型进行推断
现在你了解了如何包装一个类型的属性,那么接下来就是如何拆包。 其实这也非常容易:

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

注意这个拆包推断只适用于同态的映射类型。 如果映射类型不是同态的,那么需要给拆包函数一个明确的类型参数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值