TypeScript 学习笔记7: Generics

时间:2022-02-19 15:43:40

原文链接:https://leanpub.com/essentialtypescript/read#leanpub-auto-generics
在静态语言中,如C++、C#、Java,generic 是为了让代码具备一定的动态类型,以便于减少重复性。而Javascript本身就是动态类型语言,为什么还需要generic呢?我想,是为了增加可读性,同时增加“静态性”,给编译器提供一些类型信息,让它给我们提供一些限制,以免代码写得过于随意了。

1. generic functions

function clone(value) {
    let serialized = JSON.stringify(value);
    return JSON.parse(serialized);
}

这是js中典型的clone函数,这个函数的输入和输出应该是同一种类型的对象。如何保证这一点呢?使用 Generic:

function clone<T>(value: T) {
    let serialized = JSON.stringify(value);
    return JSON.parse(serialized);
}

说明:
1. 只是第一行有变化,在函数名和参数列表之间加了 <T>, 给参数指定类型 T;
2. T 只是一种惯用法,可以写任何字符串,只要符合变量命名规则即可。
当调用这个函数时,鼠标悬停在函数名上,可以看到TypeScript识别出来的类型:
TypeScript 学习笔记7: Generics

2. generic classes

在js中,Array 本身就是generic类型的:

var nums: number[] = [1,2];

var nums: Array<Number> = [1,2];

这两种写法完全等价。
定义一个 generic 的键值对class:

class KeyValuePair<TKey, TValue> {
    constructor(public key: TKey,
        public value: TValue) {
        }
}

let pair1 = new KeyValuePair(1, 'First');
let pair2 = new KeyValuePair('Second', Date.now());
let pair3 = new KeyValuePair(3, 'Third');

把鼠标悬停在任意变量上,TypeScript 都能识别出类型,如:
TypeScript 学习笔记7: Generics
我们也可以明确指定 Key 和Value的类型:

let pair1 = new KeyValuePair<number, string>(1, 'First');
let pair2 = new KeyValuePair<string, Date>('Second', Date.now());
let pair3 = new KeyValuePair<number, string>(3, 'Third');

如果构造函数传入的数值与指定的类型不同,编译器会报错。这时,在Visual Studio Code中,我们会看到 pair2 的 Date.now() 下面有错误提示。因为,Date.now() 的返回值时number,不是Date。
在更复杂的情况下,TypeScript 也可以推断出类型信息,例如:

class KeyValuePairPrinter<T,U> {
    constructor(private pairs: KeyValuePair<T,U>[]) {        
    }

    print() {
        for(let p of this.pairs) {
            console.log(`${p.key}: ${p.value}`);
        }
    }
}

let printer = new KeyValuePairPrinter([pair1, pair2, pair3]);
printer.print();

第12行有编译错误,因为,pair2 和 其它两个变量类型不同。

3. generic constraints

回顾一下这个函数:

function totalLength(x: {length: number}, y:{length: number}) {
    var total: number = x.length + y.length;
    return total;
}

看起来挺完美,但我们无法避免这种情况:

var length = totalLength('Jess', [1,2,3]);

把字符串和数组的length相加没有什么意义。
有了generic,可以这么改:

function totalLength<T>(x: T, y: T) {
    var total: number = x.length + y.length;
    return total;
}

这样可以保证两个参数类型相同,但,又不能保证它们都有 length 属性。
Generic constraints 来了:

function totalLength<T extends { length: number }>(x: T, y: T) {
    var total: number = x.length + y.length;
    return total;
}

说明:
1. extends 关键字,前面是T,后面是一个匿名 interface;
2. 我们曾经见过extends,在类的 “继承” 时用过它;

这里可以用任何interface,不必是匿名的,如:

interface IHaveALength {
    length: number
}

function totalLength<T extends IHaveALength>(x: T, y: T) {
    var total: number = x.length + y.length;
    return total;
}

这样,TypeScript可以明确的识别出下面的代码是否合法:

var l1 = totalLength([1,2], [1,2,3]);
var l2 = totalLength('Less', [1,2,3]);

Generic 也兼容子类型,例如,我们定义一个Array的子类:

class CustomArray<T> extends Array<T> {
    toJson(): string {
        return JSON.stringify(this);
    }
}

这种用法是合法的:

var length = totalLength([1,2], new CustomArray<number>());