TypeScript学习记录

2,274 阅读32分钟

1.TypeScript数据类型

typescript中为了使编写的代码更规范,更有利于维护,增加了类型校验(类型声明)

基础类型

在typescript中主要给我们提供了以下数据类型:

  • 字符串类型(string)
  • 数字类型(number)
  • 布尔类型(boolean)
  • null和undefined
  • 数组类型(array)
  • 元组类型(tuple)
  • 枚举类型(enum)
  • 任意类型(any)
  • void类型
  • never类型

相比于js的数据类型,typescript中多了元组类型、枚举类型、任意类型、void类型和never类型。当然这些只是基础类型,还有更多其他类型,后面的类型推论高级类型可以进一步了解。

变量定义

写ts代码变量可以指定其类型,指定类型后赋值必须为指定的类型,否则报错。

  • 如果没有指定类型,ts类型推论会帮助提供类型,请看ts类型推论
var flag:boolean = true
flag = 123 // 错误,类型不一致

数据类型

字符串类型(string)

var str:string = 'this is ts';

str='haha';  //正确

// str=true;  //错误

数字类型(number)

var num:number = 123;

num = 456; // 正确 

// num='str';    //错误

布尔类型(boolean)

var flag:boolean = true

flag = false // 正确

// flag=123;  // 错误

null 和 undefined

undefined:

{
    // 在js中,变量已声明但未初始化为undefined
    var undefinedTest:number
    // console.log(undefinedTest) // 错误写法,typescript报错,赋值了才正确

    // 在typescript中,已声明未初始化的值要直接访问的话类型需要定义为undefined
    var undefinedTest2:undefined
    console.log(undefinedTest2) // 正确写法,输出undefined 
}
{
    // 可能是number类型 可能是undefined
    var undefinedTest3:number | undefined;
    console.log(num);
}

null:

// null是一个空指针对象,undefined是未初始化的变量。因此,可以把undefined看作是空的变量,而null看作是空的对象
var nullTest:null
nullTest = null
// nullTest = {} // 错误,定义了类型是null,值必须为null

数组类型(array)

ts有两种方式可以定义数组。 第一种,可以在元素类型后面接上 [],表示由此类型元素组成的一个数组:

// 第一种
var arr:number[] = [1, 2, 3]

第二种方式是使用数组泛型,Array<元素类型>:

// 第二种
var arr2:Array<number> = [1, 2, 3]

元组类型(tuple)

和数组类似,元素的类型不一样:

let arr:[number,string] = [123,'this is ts']

枚举类型(enum)

用法:

enum 枚举名{ 
    标识符[=整型常数], 
    标识符[=整型常数], 
    ... 
    标识符[=整型常数], 
}
enum Flag {success = 1,error = 2};

let s:Flag = Flag.success // 使用枚举类型中的值
console.log('正确状态',s)
let f:Flag = Flag.error
console.log('错误状态',f)

任意类型(any)

为那些在编程阶段还不清楚类型的变量指定一个类型

var number:any = 123
number = 'str'
number = true

void类型

typescript中的void表示没有任何类型,一般用于定义方法的时候方法没有返回值。

// 表示方法没有返回任何类型
function run(): void {
    console.log('run')
}

run()

never类型

表示的是那些永不存在的值的类型,例如异常

var a:never

// a = 123 //错误写法
a = (() => {
    throw new Error('错误');
})()

2.TypeScript函数

内容概述: 函数的定义、可选参数、默认参数、剩余参数、函数重载、箭头函数。

函数的定义

语法:

// 函数声明
function fn(x: Type, y: Type): Type {}

// 函数表达式
var fn = (x: Type, y: Type): Type => {}

// 函数表达式:指定变量fn的类型
var fn: (x: Type, y: Type) => Type = (x, y) => {}
  • 定义函数有函数声明和函数表达式两种形式。定义函数的参数和返回值可以指定其类型;当调用函数时,传入参数类型必须与定义函数参数类型保持一致。
// 函数声明法
function run(x: number, y: number): number {
    return x + y;
}

// 函数表达式法
var run2 = (x: number, y: number): string => {
    return 'run2'
}

run(1, 2);
run2(1, 2);

这段代码中,函数run和run2指定了参数类型,调用时传入参数类型必须保持一致。

  • 函数表达式法另外一种写法
var run3: (x: number, y: number) => string = function(x: number, y: number): string{
    return 'run3';
}
run3(1, 2);

当给变量run3指定类型的时候,应该是函数的参数和返回值的约束类型。如果用后面学到的ts类型推论,可以简写为:

var run4: (x: number, y: number) => string = function(x, y){ // 类型推论可以确定函数的参数和返回值类型,也就可以省略类型指定
    return 'run4';
}
run4(1, 2);
  • 函数没有返回值用void类型指定返回值类型
function voidFnc(): void{
    console.log('没有返回值的方法用void')
}
voidFnc();

可选参数

  • es5里面方法的实参和行参可以不一样,但是ts中必须一样,如果不一样就需要在可选参数后加?,这就是可选参数。
function electParam(name:string, age?:number):string {
    // 这里的age可传可不传,age就是可选参数
    if(age){
        return `${name} --- ${age}`
    }else{
        return `${name} --- 年龄保密`
    }
}
console.log('可选参数', electParam('dz'))

// 注意: 可选参数必须配置到参数的最后面

// 错误写法:可选参数不在最后面
// function electParam2(name?: string, age: number): string {
//     ...
// }

默认参数

  • es5里面没法设置默认参数,es6和ts中都可以设置默认参数
// age为默认参数
function defaultParam(name:string, age:number = 20):String {
    return `${name} --- ${age}`
}

console.log('默认参数', defaultParam('dz'))

剩余参数

  • 当有很多参数时候或参数个数不确定,可以用三点运算符
// sum参数传过来的是一个数组
function sum(...result: number[]): number {
    var sum = 0;

    for (var i = 0; i < result.length; i++) {

        sum += result[i];
    }

    return sum;
}
console.log('剩余参数', sum(1, 2, 3, 4, 5, 6));

// a=1 b=2 其他参数为剩余参数
function sum2(a: number, b: number, ...result: number[]): number {
    var sum = a * b;

    for (var i = 0; i < result.length; i++) {

        sum += result[i];
    }

    return sum;
}
console.log('剩余参数2', sum2(1, 2, 3, 4, 5, 6));

函数重载

同名函数,传入不同的参数,实现不同的功能,这就叫作函数重载。

  • java中方法的重载:重载指的是两个或者两个以上同名函数,但它们的参数不一样,这时会出现函数重载的情况。
  • typescript中的重载:通过为同一个函数提供多个函数类型定义来实现多种功能的目的。
  • ts为了兼容es5以及es6,重载的写法和java中有区别。

es5中同名函数,后面会覆盖前面的函数,ts中则不会:

function overloadingFn(x: number, y: number): number;
function overloadingFn(x: string, y: string): string;

// 上面定义函数的格式,下面定义函数的具体实现
function overloadingFn(x: any, y: any): any {
    return x + y;
}

overloadingFn(1, 2);
overloadingFn('a', 'b');

这段代码中,同名函数overloadingFn首先定义两个函数的格式,然后再去实现功能,原来要传入不同类型参数要用多个函数实现,现在可以用同名函数来实现,这就是函数重载。

箭头函数

箭头函数和es6中一样

setTimeout(() => {
    console.log('箭头函数')
}, 1000);

TypeScript类

es5中的类

内容概述:类的创建、静态方法、继承(对象冒充继承,原型链继承,对象冒充 + 原型链组合继承)

es5中的面向对象、构造函数、原型与原型链本质可以看这个文档caibaojian.com/javascript-… , 个人觉得写得很清晰。

1.1 类的创建

es5类在构造函数和原型链里都可以添加属性和方法,原型链上的属性会被多个实例所共享,而构造函数则不会。


function Person() {
    this.name = 'Ming'
    this.run = function() {
        console.log(this.name + '在运动')
    }
}

Person.prototype.sex = '男' // 原型链上的属性会被多个实例所共享
Person.prototype.work = function() {
    console.log(this.name + '在工作')
}


var p = new Person()
p.run()
p.work()
console.log(p.name)

1.2 静态方法

调用静态方法不需要实例化


Person.getInfo=function(){
    console.log('我是静态方法');
}
Person.getInfo();

1.3 实现继承

对象冒充(或者叫构造函数继承)继承:可以继承构造函数里面的属性和方法,但是没法继承原型链上面的属性和方法

原型继承:可以继承构造函数里面的属性和方法,也可以继承原型链上面的属性和方法,但是实例化子类的时候没法给父类传参

下面是通过对象冒充 + 原型链组合继承,解决了上面两种继承方式存在的问题


function Worker(name,age){
    this.name=name;  /*属性*/
    this.age=age;
    this.run=function(){  /*实例方法*/
        alert(this.name+'在运动');
    }

}      
Worker.prototype.sex="男";
Worker.prototype.work=function(){
    alert(this.name+'在工作');
}
    
function Web(name,age){
    Worker.call(this,name,age);  // 对象冒充继承,可以继承构造函数里面的属性和方法,实例化子类可以给父类传参
}
// Web.prototype = new Worker();  // 原型链继承方法一:继承Worker构造函数和原型上所有的方法和属性
Web.prototype = Worker.prototype;  //原型链继承方法二:优化了方法一重复继承构造函数属性和方法的问题(本质可以看看http://caibaojian.com/javascript-object-5.html)

var w = new Web('赵四',20);   
w.run();
w.work();

从上面可以看出,对象冒充继承是在子类Web构造函数里面通过call方法继承父类Worker的构造函数的属性和方法;原型链继承通过子类Web的原型对象等于父类Worker的原型对象来实现继承;最后这两种继承的组合方式实现了完美继承。

3.typescript中的类

内容概述: ts中类的定义、继承、类修饰符、静态属性和静态方法、多态、抽象类和抽象方法

2.1 ts中类的定义

ts中类的定义和es6类的定义一样


class PersonDefine {
    name: string // 属性,前面省略了public关键词
    constructor(name:string) { //构造函数
        this.name = name
    }
    run():string { // 原型
        return `${this.name}在运动`
    }
}
var define = new PersonDefine('类的定义')
alert(define.run())

2.2 继承

ts中继承比es5简单很多,用extends super实现继承


class WebExtend extends PersonDefine {
    constructor(name:string) {
        super(name) // super继承父类的构造函数,并向父类构造函数传参
    }
    work():string {
        return `${this.name}在工作`
    }
}

var extend = new WebExtend('继承')
alert(extend.run())
alert(extend.work())

2.3 ts类里面的修饰符

修饰符:typescript里面定义属性的时候给我们提供了三种修饰符

  • public: 公有修饰符,在当前类里面、子类、类外面都可以访问
  • protected:保护类型,在当前类里面、子类里面可以访问,在类外部没法访问
  • private :私有修饰符,在当前类里面可以访问,子类、类外部都没法访问

注意:属性如果不加修饰符,默认就是公有修饰符


// 以private为例
class PersonPrivate{
    private name:string;  /*被private修饰的属性 => 私有属性*/
    constructor(name:string){
        this.name=name;
    }
    run():string{
        return `${this.name}在运动` // 私有属性只能在当前类里面可以访问
    }
}

class Web extends PersonPrivate{
    constructor(name:string){
        super(name)
    }
    work(){
        // return `${this.name}在工作` // 报错,子类不能访问父类的私有属性
    }
}
 
var privateName = new PersonPrivate('private')
alert(privateName.run())
// console.log(privateName.name) // 报错,外部不能访问类的私有属性

2.4静态属性和静态方法

为什么要用静态属性和静态方法?jq里面的$.ajax就是用的静态方法


function $(element) {
    return new Base(element)
}

function Base(element) {
    this.element = document.getElementById(element)
    this.css = function(arr, value) {
        this.element.style[arr] = value
    }
}
$('box').css('color','red')
$.ajax = function() {}  // 想要在$上使用方法怎么办,用静态方法

ts中实现静态属性和静态方法用static


class PersonStatic{
    /*公有属性*/
    public name:string;
    constructor(name:string) {
        this.name=name;
    }
    /*实例方法(需要被实例化,所以为实例方法)*/
    run(){  
        return `${this.name}在运动`
    }
    /*静态属性*/
    static sex = '男'
    /*静态方法,里面没法直接调用类里面的属性*/
    static info(){  
        // return 'info方法' + this.name  // 静态方法不能调用本类的方法和属性,可以调用静态属性
        return 'info方法' + PersonStatic.sex
    }
}

console.log('静态方法' + PersonStatic.info())
console.log('静态属性' + PersonStatic.sex)

2.5多态

父类定义一个方法不去实现,让继承它的子类去实现,每一个子类的该方法有不同的表现

  • 多态属于继承

比如定义一个父类Animal,里面的eat方法不去实现,让子类Dog和Cat分别实现自己的eat方法


class Animal {
    name:string;
    constructor(name:string) {
        this.name=name;
    }
    eat(){   // eat方法继承它的子类去实现
    }
}
class Dog extends Animal{
    constructor(name:string){
        super(name)
    }
    eat(){
        return this.name+'吃粮食'
    }
}

class Cat extends Animal{
    constructor(name:string){
        super(name)
    }
    eat(){
        return this.name+'吃老鼠'
    }
}

2.6抽象类和抽象方法

定义:用abstract关键字定义抽象类和抽象方法,抽象类中的抽象方法不包含具体实现并且必须在派生类(抽象类的子类)中实现

  • 抽象类:它是提供其他类继承的基类,不能直接被实例化,子类继承可以被实例化
  • abstract修饰的方法(抽象方法)只能放在抽象类里面
  • 抽象类和抽象方法用来定义标准(比如定义标准为:抽象类Animal有抽象方法eat,要求它的子类必须包含eat方法)

abstract class AnimalAbst{
    public name:string;
    constructor(name:string){
        this.name=name;
    }
    abstract eat():any;  //抽象方法不包含具体实现并且必须在派生类中实现
    run(){
        console.log('其他方法可以不实现')
    }
}
// var a = new Animal() /*错误的写法,抽象类不能被实例化*/

class DogAbst extends Animal{
    //抽象类的子类必须实现抽象类里面的抽象方法
    constructor(name:any){
        super(name)
    }
    eat(){
        return this.name + '吃粮食'
    }
}

var d = new DogAbst('小花花');
console.log('抽象类和抽象方法',d.eat());

4.TypesSript接口

接口定义:接口是对传入参数进行约束;或者对类里面的属性和方法进行声明和约束,实现这个接口的类必须实现该接口里面属性和方法;typescript中的接口用interface关键字定义。

接口作用:接口定义了某一批类所需要遵守的规范,接口不关心这些类的内部状态数据,也不关心这些类里方法的实现细节,它只规定这批类里必须提供某些方法,提供这些方法的类就可以满足实际需要。typescrip中的接口类似于java,同时还增加了更灵活的接口类型,包括属性、函数、可索引和类等。

内容概述:接口分类:(属性接口、函数类型接口、可索引接口、类类型接口),接口的继承

1. 接口分类

1.1 属性接口

对传入对象的约束(也就是对json的约束)

在了解接口之前,我们来看看函数传入obj参数


function printLabel(labelInfo: {label:string}){
    return labelInfo
}
// printLabel({name:'obj'});  //错误的写法
console.log(printLabel({label: 'obj'}))

和上面类似,由此引入属性接口 => 对方法传入参数进行约束

下面为属性接口的例子,方法printFullName对传入参数FullName(为对象)进行约束


interface FullName{
    firstName: string; // 注意;结束
    secondName: string;
    age?: number // 接口的可选属性用?
}

function printFullName(name:FullName) {
    // 传入对象必须包含firstName和secondName,可传可不传age
    return name
}
var obj = {
    firstName:'小',
    secondName:'明',
    age: 20
}
console.log(printFullName(obj))

属性接口应用:原生js封装ajax


interface Config{
    type: string;
    url: string;
    data?: string;
    dataType: string;
}
function ajax(config: Config) {
    var xhr = new XMLHttpRequest
    xhr.open(config.type, config.url, true)
    xhr.send(config.data)
    xhr.onreadystatechange = function() {
        if(xhr.readyState == 4 && xhr.status == 200) {
            if(config.dataType == 'json'){
                console.log(JSON.parse(xhr.responseText))
            }else{
                console.log(xhr.responseText)
            }
        }
    }
}

ajax({
    type: 'get',
    data: 'name=xiaoming',
    url: 'http://a.itying.com/api/productlist',
    dataType: 'json'
})

1.2 函数类型接口

对方法传入的参数以及返回值进行约束


interface encrypt{
    (key: string, value: string): string; // 传入的参数和返回值的类型
}

var md5:encrypt = function(key:string, value:string):string{
    // encrypt对加密方法md5进行约束,同时md5方法的参数和返回值类型和encrypt要保持一致
    return key + value
}

console.log(md5('name', '小明'))

1.3 可索引接口

对索引和传入参数的约束(一般用于对数组、对象的约束)

ts中定义数组:


var arr1:number[] = [1,2]
var arr2:Array<string> = ['1', '2']

现在用接口来实现:


// 对数组的的约束
interface UserArr{
    // 索引为number,参数为string
    [index:number]: string
}
var userarr:UserArr = ['a', 'b']
console.log(userarr)


// 对象的约束
interface UserObj{
    // 索引为string,参数为string
    [index:string]: string
}
var userobj:UserObj = { name: '小明', sex: '男' }
console.log(userobj)

1.4 类类型接口

对类的约束,和抽象类抽象有点相似


interface Animal{
    // 对类里面的属性和方法进行约束
    name:string;
    eat(str:string):void;
}
// 类实现接口要用implements关键字,必须实现接口里面声明的方法和属性
class Cat implements Animal{
    name:string;
    constructor(name:string){
        this.name = name
    }
    eat(food:string){
        console.log(this.name + '吃' + food)
    }
}
var cat = new Cat('小花')
cat.eat('老鼠')

2. 接口的继承

和类的继承一样,用extends实现接口继承

下面同时实现类的继承和接口的继承


interface Animal {
    eat(): void;
}
// 继承Animal接口,则实现Person接口的类必须也实现Animal接口里面的方法
interface Person extends Animal {
    work(): void;
}

class Programmer {
    public name: string;
    constructor(name: string) {
        this.name = name;
    }
    coding(code: string) {
        console.log(this.name + code)
    }
}

// 继承类并且实现接口
class Web extends Programmer implements Person {
    constructor(name: string) {
        super(name)
    }
    eat() {
        console.log(this.name + '吃')
    }
    work() {
        console.log(this.name + '工作');
    }
}

var w = new Web('小李');
w.eat();
w.coding('写ts代码');

5.TypesSript泛型

泛型:很多时候,类型是写死的,不利于复用,泛型可以简单理解为给类型这种值设置变量,解决类、接口、方法的复用性,以及对不特定数据类型的支持。

  • 语法: <类型变量名>,一般系统使用单字母大写,比如 T、S...

内容概述:泛型函数、泛型类、泛型接口

泛型函数

传入的参数类型和返回的参数类型可以指定

我们来看看函数用ts数据类型,想要同时返回string类型和number类型


function getData1(value:string):string{
    return value;
}
function getData2(value:number):number{
    return value;
}

这样要写不同的函数,不能按照需求返回不同类型数据,造成代码冗余 => 由此引入泛型

表示泛型,调用的时候指定T的数据类型


function dataT<T>(value:T):T{
    // 传入参数为T 返回值为T
    return value
}
dataT<number>(1) // 调用指定泛型为number类型,则传入参数也必须为number类型
dataT<string>('string')

function dataAny<T>(value:T):any{
    return '传入参数为T,任意类型返回值';
}
dataAny<number>(123); // 参数必须是number
dataAny<string>('这是一个泛型');

泛型类

泛型类使用(<>)括起泛型类型,跟在类名后面。

有个最小堆算法,需要同时支持返回数字和字符串两种类型

使用泛型之前:只能在类的类部指定数据类型,实现需求还要写一套string类型的类


class MinClass{
    public list:number[]=[];
    add(num:number){
        this.list.push(num)
    }
    min():number{
        var minNum=this.list[0];
        for(var i=0;i<this.list.length;i++){
            if(minNum>this.list[i]){
                minNum=this.list[i];
            }
        }
        return minNum;
    }
}

var m=new MinClass();
m.add(1);
m.add(2);
alert(m.min());

使用泛型之后:只用一套类来实现


class MinClassT<T>{
    public list:T[]=[];
    add(value:T):void{
        this.list.push(value);
    }
    min():T{        
        var minNum=this.list[0];
        for(var i=0;i<this.list.length;i++){
            if(minNum>this.list[i]){
                minNum=this.list[i];
            }
        }
        return minNum;
    }
}
var m1=new MinClassT<number>();   /*实例化类 并且指定了类的T代表的类型是number*/
m.add(1);
m.add(2);
alert(m1.min())

var m2=new MinClassT<string>();   /*实例化类 并且指定了类的T代表的类型是string*/
m2.add('c');
m2.add('a');
alert(m2.min())

泛型接口

有一个函数类型接口


interface ConfigFn{
    (value:string):string;
}
var setData:ConfigFn = function(value:string):string{
    return value
}
setData('name');
// setData(20); // 错误

setData(20);写法错误,想要传入number类型的参数又要写一个函数类型接口 => 用泛型接口

泛型接口有两种写法:


// 泛型接口定义方式一
interface ConfigFnOne{
    <T>(value:T):T;
}
var setDataOne:ConfigFnOne = function<T>(value:T):T{
    return value
}
// 既可以传入string也可以传入number类型参数
setDataOne<string>('name');
setDataOne<number>(20);


// 泛型接口定义方式二
interface ConfigFnTwo<T>{
    (value:T):T;
}
function setDataTwo<T>(value:T):T{
    return value
}
var setDataTwoFn:ConfigFnTwo<string> = setDataTwo
setDataTwoFn('name');

6.TypesSript类型推论

有的时候不一定需要强制使用类型声明,在有些没有明确指出类型的地方,ts类型推论会帮助提供类型。

内容概述:变量初始化类型推论、上下文类型推论。

变量初始化

ts会根据变量初始化的时候赋予的值进行类型推断。

let a = '类型推论';
// a = true; // Type 'true' is not assignable to type 'string'

上面代码中,a初始化没有指定类型,ts会推论出a的类型为string,当a = true重新赋值的时候类型不匹配会报相应错误,vscode编译器会提示错误。

上下文推断

ts也会根据上下文进行类型的推断,比如在事件函数中,函数的第一个参数会根据当前绑定的事件类型推断处理事件对象。

document.onkeydown = function(e) {
    // console.log(e.button);  //<- Error Property 'button' does not exist on type 'KeyboardEvent'
};

这个例子会得到一个类型错误,ts类型检查器根据当前绑定的事件类onkeydown自动推导e的类型为KeyboardEvent,vscode编译器里鼠标放上去就有e推导出来的类型(e:KeyboardEvent)

7.TypesSript高级类型

交叉类型(Intersection Types)

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如,Person & Serializable & Loggable同时是PersonSerializableLoggable。 就是说这个类型的对象同时拥有了这三种类型的成员。

我们大多是在混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。 (在JavaScript里发生这种情况的场合很多!) 下面是如何创建混入的一个简单例子("target": "es5"):

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() {
        // ...
    }
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();

联合类型(Union Types)

联合类型与交叉类型很有关联,但是使用上却完全不同。 偶尔你会遇到这种情况,一个代码库希望传入numberstring类型的参数。 例如下面的函数:

/**
 * Takes a string and adds "padding" to the left.
 * If 'padding' is a string, then 'padding' is appended to the left side.
 * If 'padding' is a number, then that number of spaces is added to the left side.
 */
function padLeft(value: string, padding: any) {
    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}'.`);
}

padLeft("Hello world", 4); // returns "    Hello world"

padLeft存在一个问题,padding参数的类型指定成了any。 这就是说我们可以传入一个既不是number也不是string类型的参数,但是TypeScript却不报错。

let indentedString = padLeft("Hello world", true); // 编译阶段通过,运行时报错

在传统的面向对象语言里,我们可能会将这两种类型抽象成有层级的类型。 这么做显然是非常清晰的,但同时也存在了过度设计。 padLeft原始版本的好处之一是允许我们传入原始类型。 这样做的话使用起来既简单又方便。 如果我们就是想使用已经存在的函数的话,这种新的方式就不适用了。

代替any, 我们可以使用联合类型做为padding的参数:

/**
 * Takes a string and adds "padding" to the left.
 * If 'padding' is a string, then 'padding' is appended to the left side.
 * If 'padding' is a number, then that number of spaces is added to the left side.
 */
function padLeft(value: string, padding: string | number) {
    // ...
}

let indentedString = padLeft("Hello world", true); // errors during compilation

联合类型表示一个值可以是几种类型之一。 我们用竖线(|)分隔每个类型,所以number | string | boolean表示一个值可以是numberstring,或boolean

如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。

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

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

function getSmallPet(): Fish | Bird {
    // ...
}

let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors

这里的联合类型可能有点复杂,但是你很容易就习惯了。 如果一个值的类型是A | B,我们能够确定的是它包含了AB中共有的成员。 这个例子里,Bird具有一个fly成员。 我们不能确定一个Bird | Fish类型的变量是否有fly方法。 如果变量在运行时是Fish类型,那么调用pet.fly()就出错了。

类型守卫与类型区分(Type Guards and Differentiating Types)

联合类型适合于那些值可以为不同类型的情况。 但当我们想确切地了解是否为Fish时怎么办? JavaScript里常用来区分2个可能值的方法是检查成员是否存在。 如之前提及的,我们只能访问联合类型中共同拥有的成员。

let pet = getSmallPet();

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

为了让这段代码工作,我们要使用类型断言:

let pet = getSmallPet();

if ((<Fish>pet).swim) {
    (<Fish>pet).swim();
}
else {
    (<Bird>pet).fly();
}

用户自定义的类型守卫

这里可以注意到我们不得不多次使用类型断言。 假若我们一旦检查过类型,就能在之后的每个分支里清楚地知道pet的类型的话就好了。

TypeScript里的类型守卫机制让它成为了现实。 类型守卫就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 要定义一个类型守卫,我们只要简单地定义一个函数,它的返回值是一个类型谓词

function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

在这个例子里,pet is Fish就是类型谓词。 谓词为parameterName is Type这种形式,parameterName必须是来自于当前函数签名里的一个参数名。

每当使用一些变量调用isFish时,TypeScript会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。

// 'swim' 和 'fly' 调用都没有问题了

if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}

注意TypeScript不仅知道在if分支里petFish类型; 它还清楚在else分支里,一定不是Fish类型,一定是Bird类型。

typeof类型守卫

现在我们回过头来看看怎么使用联合类型书写padLeft代码。 我们可以像下面这样利用类型断言来写:

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}'.`);
}

这些*typeof类型守卫*只有两种形式能被识别:typeof v === "typename"typeof v !== "typename""typename"必须是"number""string""boolean""symbol"。 但是TypeScript并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型守卫。

instanceof类型守卫

如果你已经阅读了typeof类型守卫并且对JavaScript里的instanceof操作符熟悉的话,你可能已经猜到了这节要讲的内容。

instanceof类型守卫是通过构造函数来细化类型的一种方式。 比如,我们借鉴一下之前字符串填充的例子:

interface Padder {
    getPaddingString(): string
}

class SpaceRepeatingPadder implements Padder {
    constructor(private numSpaces: number) { }
    getPaddingString() {
        return Array(this.numSpaces + 1).join(" ");
    }
}

class StringPadder implements Padder {
    constructor(private value: string) { }
    getPaddingString() {
        return this.value;
    }
}

function getRandomPadder() {
    return Math.random() < 0.5 ?
        new SpaceRepeatingPadder(4) :
        new StringPadder("  ");
}

// 类型为SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {
    padder; // 类型细化为'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
    padder; // 类型细化为'StringPadder'
}

instanceof的右侧要求是一个构造函数,TypeScript将细化为:

  1. 此构造函数的prototype属性的类型,如果它的类型不为any的话
  2. 构造签名所返回的类型的联合

以此顺序。

可以为null的类型

TypeScript具有两种特殊的类型,nullundefined,它们分别具有值nullundefined. 我们在基础类型一节里已经做过简要说明。 默认情况下,类型检查器认为nullundefined可以赋值给任何类型。 nullundefined是所有其它类型的一个有效值。 这也意味着,你阻止不了将它们赋值给其它类型,就算是你想要阻止这种情况也不行。 null的发明者,Tony Hoare,称它为价值亿万美金的错误

--strictNullChecks标记可以解决此错误:当你声明一个变量时,它不会自动地包含nullundefined。 你可以使用联合类型明确的包含它们:

let s = "foo";
s = null; // 错误, 'null'不能赋值给'string'
let sn: string | null = "bar";
sn = null; // 可以

sn = undefined; // error, 'undefined'不能赋值给'string | null'

注意,按照JavaScript的语义,TypeScript会把nullundefined区别对待。 string | nullstring | undefinedstring | undefined | null是不同的类型。

可选参数和可选属性

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

function f(x: number, y?: number) {
    return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'

可选属性也会有同样的处理:

class C {
    a: number;
    b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'

类型守卫和类型断言

由于可以为null的类型是通过联合类型实现,那么你需要使用类型守卫来去除null。 幸运地是这与在JavaScript里写的代码一致:

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

这里很明显地去除了null,你也可以使用短路运算符:

function f(sn: string | null): string {
    return sn || "default";
}

如果编译器不能够去除nullundefined,你可以使用类型断言手动去除。 语法是添加!后缀:identifier!identifier的类型里去除了nullundefined

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

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

本例使用了嵌套函数,因为编译器无法去除嵌套函数的null(除非是立即调用的函数表达式)。 因为它无法跟踪所有对嵌套函数的调用,尤其是你将内层函数做为外层函数的返回值。 如果无法知道函数在哪里被调用,就无法知道调用时name的类型。

类型别名

类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

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

与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型。

type LinkedList<T> = T & { next: LinkedList<T> };

interface Person {
    name: string;
}

var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;

然而,类型别名不能出现在声明右侧的任何地方。

type Yikes = Array<Yikes>; // error

接口 vs. 类型别名

像我们提到的,类型别名可以像接口一样;然而,仍有一些细微差别。

其一,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。 在下面的示例代码里,在编译器中将鼠标悬停在interfaced上,显示它返回的是Interface,但悬停在aliased上时,显示的却是对象字面量类型。

type Alias = { num: number }
interface Interface {
    num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;

另一个重要区别是类型别名不能被extendsimplements(自己也不能extendsimplements其它类型)。 因为软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名。

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

字符串字面量类型

字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型守卫和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。

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

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

Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'

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

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

数字字面量类型

TypeScript还具有数字字面量类型。

function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
    // ...
}

我们很少直接这样使用,但它们可以用在缩小范围调试bug的时候:

function foo(x: number) {
    if (x !== 1 || x !== 2) {
        //         ~~~~~~~
        // Operator '!==' cannot be applied to types '1' and '2'.
    }
}

换句话说,当x2进行比较的时候,它的值必须为1,这就意味着上面的比较检查是非法的。

枚举成员类型

如我们在枚举一节里提到的,当每个枚举成员都是用字面量初始化的时候枚举成员是具有类型的。

在我们谈及“单例类型”的时候,多数是指枚举成员类型和数字/字符串字面量类型,尽管大多数用户会互换使用“单例类型”和“字面量类型”。

可辨识联合(Discriminated Unions)

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

  1. 具有普通的单例类型属性—可辨识的特征
  2. 一个类型别名包含了那些类型的联合—联合
  3. 此属性上的类型守卫。
interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}

首先我们声明了将要联合的接口。 每个接口都有kind属性但有不同的字符串字面量类型。 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;
    }
}

完整性检查

当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。 比如,如果我们添加了TriangleShape,我们同时还需要更新area:

type Shape = Square | Rectangle | Circle | Triangle;
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;
    }
    // should error here - we didn't handle case "triangle"
}

有两种方式可以实现。 首先是启用--strictNullChecks并且指定一个返回值类型:

function area(s: Shape): number { // error: returns number | undefined
    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;
    }
}

因为switch没有包含所有情况,所以TypeScript认为这个函数有时候会返回undefined。 如果你明确地指定了返回值类型为number,那么你会看到一个错误,因为实际上返回值的类型为number | undefined。 然而,这种方法存在些微妙之处且--strictNullChecks对旧代码支持不好。

第二种方法使用never类型,编译器用它来进行完整性检查:

function assertNever(x: never): never {
    throw new Error("Unexpected object: " + x);
}
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;
        default: return assertNever(s); // error here if there are missing cases
    }
}

这里,assertNever检查s是否为never类型—即为除去所有可能情况后剩下的类型。 如果你忘记了某个case,那么s将具有一个真实的类型并且你会得到一个错误。 这种方式需要你定义一个额外的函数,但是在你忘记某个case的时候也更加明显。

多态的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索引类型查询操作符。 对于任何类型Tkeyof 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: Tname: 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'

索引类型和字符串索引签名

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

它的语法与索引签名的语法类型,内部使用了for .. in。 具有三个部分:

  1. 类型变量K,它会依次绑定到每个属性。
  2. 字符串字面量联合的Keys,它包含了要迭代的属性名的集合。
  3. 属性的结果类型。

在个简单的例子里,Keys是硬编码的属性名列表并且属性类型永远是boolean,因此这个映射类型等同于:

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

在真正的应用里,可能不同于上面的ReadonlyPartial。 它们会基于一些已存在的类型,且按照一定的方式转换字段。 这就是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<Person>.name也将是只读的且为可选的。

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

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的标准库里:

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

ReadonlyPartialPick是同态的,但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);

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

有条件类型

TypeScript 2.8引入了有条件类型,它能够表示非统一的类型。 有条件的类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一:

T extends U ? X : Y

上面的类型意思是,若T能够赋值给U,那么类型是X,否则为Y

有条件的类型T extends U ? X : Y或者解析X,或者解析Y,再或者延迟解析,因为它可能依赖一个或多个类型变量。 若TU包含类型参数,那么是否解析为XY或推迟,取决于类型系统是否有足够的信息来确定T总是可以赋值给U

下面是一些类型可以被立即解析的例子:

declare function f<T extends boolean>(x: T): T extends true ? string : number;

// Type is 'string | number
let x = f(Math.random() < 0.5)

另外一个例子涉及TypeName类型别名,它使用了嵌套了有条件类型:

type TypeName<T> =
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";

type T0 = TypeName<string>;  // "string"
type T1 = TypeName<"a">;  // "string"
type T2 = TypeName<true>;  // "boolean"
type T3 = TypeName<() => void>;  // "function"
type T4 = TypeName<string[]>;  // "object"

下面是一个有条件类型被推迟解析的例子:

interface Foo {
    propA: boolean;
    propB: boolean;
}

declare function f<T>(x: T): T extends Foo ? string : number;

function foo<U>(x: U) {
    // Has type 'U extends Foo ? string : number'
    let a = f(x);

    // This assignment is allowed though!
    let b: string | number = a;
}

这里,a变量含有未确定的有条件类型。 当有另一段代码调用foo,它会用其它类型替换U,TypeScript将重新计算有条件类型,决定它是否可以选择一个分支。

与此同时,我们可以将有条件类型赋值给其它类型,只要有条件类型的每个分支都可以赋值给目标类型。 因此在我们的例子里,我们可以将U extends Foo ? string : number赋值给string | number,因为不管这个有条件类型最终结果是什么,它只能是stringnumber

分布式有条件类型

如果有条件类型里待检查的类型是naked type parameter,那么它也被称为“分布式有条件类型”。 分布式有条件类型在实例化时会自动分发成联合类型。 例如,实例化T extends U ? X : YT的类型为A | B | C,会被解析为(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)

例子

type T10 = TypeName<string | (() => void)>;  // "string" | "function"
type T12 = TypeName<string | string[] | undefined>;  // "string" | "object" | "undefined"
type T11 = TypeName<string[] | number[]>;  // "object"

T extends U ? X : Y的实例化里,对T的引用被解析为联合类型的一部分(比如,T指向某一单个部分,在有条件类型分布到联合类型之后)。 此外,在X内对T的引用有一个附加的类型参数约束U(例如,T被当成在X内可赋值给U)。

例子

type BoxedValue<T> = { value: T };
type BoxedArray<T> = { array: T[] };
type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>;

type T20 = Boxed<string>;  // BoxedValue<string>;
type T21 = Boxed<number[]>;  // BoxedArray<number>;
type T22 = Boxed<string | number[]>;  // BoxedValue<string> | BoxedArray<number>;

注意在Boxed<T>true分支里,T有个额外的约束any[],因此它适用于T[number]数组元素类型。同时也注意一下有条件类型是如何分布成联合类型的。

有条件类型的分布式的属性可以方便地用来过滤联合类型:

type Diff<T, U> = T extends U ? never : T;  // Remove types from T that are assignable to U
type Filter<T, U> = T extends U ? T : never;  // Remove types from T that are not assignable to U

type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"
type T32 = Diff<string | number | (() => void), Function>;  // string | number
type T33 = Filter<string | number | (() => void), Function>;  // () => void

type NonNullable<T> = Diff<T, null | undefined>;  // Remove null and undefined from T

type T34 = NonNullable<string | number | undefined>;  // string | number
type T35 = NonNullable<string | string[] | null | undefined>;  // string | string[]

function f1<T>(x: T, y: NonNullable<T>) {
    x = y;  // Ok
    y = x;  // Error
}

function f2<T extends string | undefined>(x: T, y: NonNullable<T>) {
    x = y;  // Ok
    y = x;  // Error
    let s1: string = x;  // Error
    let s2: string = y;  // Ok
}

有条件类型与映射类型结合时特别有用:

type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;

type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;

interface Part {
    id: number;
    name: string;
    subparts: Part[];
    updatePart(newName: string): void;
}

type T40 = FunctionPropertyNames<Part>;  // "updatePart"
type T41 = NonFunctionPropertyNames<Part>;  // "id" | "name" | "subparts"
type T42 = FunctionProperties<Part>;  // { updatePart(newName: string): void }
type T43 = NonFunctionProperties<Part>;  // { id: number, name: string, subparts: Part[] }

与联合类型和交叉类型相似,有条件类型不允许递归地引用自己。比如下面的错误。

例子

type ElementType<T> = T extends any[] ? ElementType<T[number]> : T;  // Error

有条件类型中的类型推断

现在在有条件类型的extends子语句中,允许出现infer声明,它会引入一个待推断的类型变量。 这个推断的类型变量可以在有条件类型的true分支中被引用。 允许出现多个同类型变量的infer

例如,下面代码会提取函数类型的返回值类型:

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

有条件类型可以嵌套来构成一系列的匹配模式,按顺序进行求值:

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

type T0 = Unpacked<string>;  // string
type T1 = Unpacked<string[]>;  // string
type T2 = Unpacked<() => string>;  // string
type T3 = Unpacked<Promise<string>>;  // string
type T4 = Unpacked<Promise<string>[]>;  // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>;  // string

下面的例子解释了在协变位置上,同一个类型变量的多个候选类型会被推断为联合类型:

type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>;  // string
type T11 = Foo<{ a: string, b: number }>;  // string | number

相似地,在抗变位置上,同一个类型变量的多个候选类型会被推断为交叉类型:

type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>;  // string
type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>;  // string & number

当推断具有多个调用签名(例如函数重载类型)的类型时,用最后的签名(大概是最自由的包含所有情况的签名)进行推断。 无法根据参数类型列表来解析重载。

declare function foo(x: string): number;
declare function foo(x: number): string;
declare function foo(x: string | number): string | number;
type T30 = ReturnType<typeof foo>;  // string | number

无法在正常类型参数的约束子语句中使用infer声明:

type ReturnType<T extends (...args: any[]) => infer R> = R;  // 错误,不支持

但是,可以这样达到同样的效果,在约束里删掉类型变量,用有条件类型替换:

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

预定义的有条件类型

TypeScript 2.8在lib.d.ts里增加了一些预定义的有条件类型:

  • Exclude<T, U> -- 从T中剔除可以赋值给U的类型。
  • Extract<T, U> -- 提取T中可以赋值给U的类型。
  • NonNullable<T> -- 从T中剔除nullundefined
  • ReturnType<T> -- 获取函数返回值类型。
  • InstanceType<T> -- 获取构造函数类型的实例类型。

Example

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

注意:Exclude类型是建议的Diff类型的一种实现。我们使用Exclude这个名字是为了避免破坏已经定义了Diff的代码,并且我们感觉这个名字能更好地表达类型的语义。我们没有增加Omit<T, K>类型,因为它可以很容易的用Pick<T, Exclude<keyof T, K>>来表示。

8.TypeScript装饰器

随着TypeScript和ES6里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。 装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。 Javascript里的装饰器目前处在 建议征集的第二阶段,但在TypeScript里已做为一项实验性特性予以支持。

若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json里启用experimentalDecorators编译器选项:

  • 命令行:
tsc --target ES5 --experimentalDecorators
  • tsconfig.json:
{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}

装饰器

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。

语法:

  • 首先定义装饰器函数:

定义装饰器函数,里面写一些扩展功能,这个函数会在该装饰器被使用的时候调用。

  • 使用装饰器:

在需要被装饰的类或方法前通过@expression这种形式,也就是@装饰器函数来调用装饰器。

下面是使用类装饰器(@Age)的例子,应用在Cat和Dog类上:

定义@Age装饰器函数:

function Age(v: number) {
    // 这个返回的函数才是真正的装饰器要执行的函数,作用是传参
    return function<T extends {new(...args: any[]): {}}>(constructor: T): T {
        class PersonAge extends constructor {
            age: number = v;
        }
        return PersonAge;
    }
}

在Cat和Dog类上都可以使用装饰器,本来Cat和Dog都没有age属性的,加上装饰器传入参数调用后,就有了age了。

@Age(1)
class Cat {
    name = '小猫咪'
}
@Age(2)
class Dog {
    name = '小奶狗'
}

let c1 = new Cat();
console.log(c1); // Animal {name: "小猫咪", age: 1}

let d1 = new Dog();
console.log(d1); // Animal {name: "小奶狗", age: 2}