很早之前写的一系列关于TypeScript的学习笔记,保存在了 Github 仓库。不是很方便查看,现迁移到Blog了

基础类型

字符串/数字/布尔值

只需要在申明后面加 :type 即可

let str: string = '12345'

let num: number = 12345

let bool: boolean = true

数组

要求数组中每一项的类别都相同

let ary: number[] = [1, 2, 3]

let ary: Array<number> = [1, 2, 3]

元组

数组申明要求数组中每一项都相同,元组可为数组项申明不同类型

let tuple: [string, number]

tuple = ['123', 123]

tuple[2] = 234 // 正确

tuple[1] = '234' // 正确

tuple[3] = true // 错误

可以为没有申明的类型的数组项进行赋值,但是 赋值类型必须是申明列表中存在的类型

枚举

枚举类型是 TypeScript 对 JavaScript 的扩展.

enum Color { Red = 1, Green, Blue }

let c: Color = Color.Green  // 2

let d: string = Color[2]  // 'Green'

默认从下标0开始编号,但是可以手动指定.

作用就是根据下标找值,或者根据值找下标。

Any

未知或者可变的变量类型由Any进行声明

let ha: any = 4

ha = '44'  // 正确

ha = true  // 正确

ha = [] // 正确

let list: any[] = [1, true, "free"] // 正确

遇到any标记的变量,编译器会跳过类型检查,所以只要在 JavaScript 中声明没问题就行

Void

声明为 void 类型说明该变量没有任何值,只能赋予 undefined 或者 null,这种操作没什么用

当函数没有返回值时,需要声明函数返回为 void

let hh: void = undefined

function warnUser(): void {
  console.log("This is my warning message");
}

Null / Undefined

Null / Undefined 是两个类型,申明之后只能赋值本身。。但这样做没什么用...

默认情况下 null 和 undefined 是所有类型的子类型。 就是说你可以把 null 和 undefined 赋值给 number 类型的变量。

Never

表示永远不存在值的类型

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
  throw new Error(message);
}

// 推断的返回值类型为never
function fail() {
  return error("Something failed");
}

// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
  while (true) {
  }
}

never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是 never 的子类型或可以赋值给 never 类型(除了 never 本身之外)。 即使 any 也不可以赋值给 never。

Object

除number,string,boolean,symbol,null或undefined之外的类型。

变量申明

变量申明没什么特别的,就是ES6中的知识,let,const...

研究一下下面这个

示例代码

function f([first, second]: [number, number]) {
    console.log(first);
    console.log(second);
}

const input = [1, 2]

f(input); // 报错

这是 TypeScript 中文网的示例代码。。结果居然报错了。。研究了一下明白了为什么。

首先 typescript 环境下,像 const input = [1, 2] 这样的定义,数组项都为数字,默认申明为Array<number>.所以相当于const input: Array<number> = [1, 2]

看函数参数 [first, second] , 调用函数 f(input), 这里用到了变量解构,input是一个申明为所有元素都为 number 的 数组。再看参数后面的类型定义,这是用 元组 形式申明传入参数的类型。。传入的参数与参数申明类型不符,所以报错。

正确代码

function f([first, second]: Array<number>) {
    console.log(first);
    console.log(second);
}

const input = [1, 2]  // 或者 const input: Array<number> = [1, 2]

f(input); // 正确

或者

function f([first, second]: [number, number]) {
    console.log(first);
    console.log(second);
}

const input: [number, number] = [1, 2]

f(input); // 正确

对象解构

属性值重命名

示例代码:

const { name: myName }: { name: string, age: number } = { name: 'han', age: 12 }

这里很乱,逐一分析一下:

首先不含类型检查的最基本的对象变量解构是这样的(ES6语法)

const { name } = { name: 'han', age: 12 }

// 上面这一行相当于
const { name: name } = { name: 'han', age: 12 }

如果要对name值重命名,则需要在 name 后加 :

const { name: myName } = { name: 'han', age: 12 }

TypeScript 在ES6基础上,添加了类型检查,也就是上面的示例代码。

类型检查不能只检查自己需要的数据,而是检查所有要解构的对象,所以age: number这里必不可少

在使用对象解构前,一定要牢记ES6的解构语法,在此基础上,再使用 TypeScript 的类型限制

展开操作符

使用与ES6一致

看个小例子

class C {
  p = 12;
  m() {
  }
}

let c = new C();
let clone = { ...c };
clone.p;  // ok
clone.m();  // error

当展开操作符展开一个对象时,会丢失方法。展开操作符展开的是可枚举属性。

接口

原文给出两段代码

function printLabel(labelledObj: { label: string }) {
  console.log(labelledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };

printLabel(myObj);

接口版本的

interface LabelledValue {
  label: string;
}

function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

首先这里有疑问,在上面那种方式可以实现类型检查的情况下,为什么要推出接口这个新概念?先接着往下看

可选属性

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): {color: string; area: number} {
  let newSquare = {color: "white", area: 100};
  if (config.color) {
    newSquare.color = config.color;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}

let mySquare = createSquare({color: "black"});  // ok

let mySquare = createSquare({});  // ok

let mySquare = createSquare();  // error

首先看调用这里,传参空对象是可以的,因为接口规定了我们 color, width 可传可不传。。但是没告诉我们参数是可选的,所以不传参会报错。。

改造成如下代码

function createSquare(config: SquareConfig = {}): {color: string; area: number} { 
  ...
}

let mySquare = createSquare();  // ok

初次看这个函数是有点懵逼的。function createSquare(config: SquareConfig): {color: string; area: number}中的: {color: string; area: number}是什么观察了半天?各种测试之后,才想起,这是规定返回值的类型的...这种函数多练练多写写就好了

再来看我们第一部分给出的问题,为什么要推出接口这个概念,这里还不好说,但是可以不用接口的方式实现上面的函数

function createSquare(config: {color?: string, width?: number} = {}): {color: string; area: number} { 
  ...
}

let mySquare = createSquare({color: "red"});  // ok

let mySquare = createSquare({});  // ok

let mySquare = createSquare();  // ok

只读属性

interface Point {
  readonly x: number;
  readonly y: number;
}

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

只读属性只能在对象创建的时候给出初始值,后续就没办法进行修改。

我们接着改造上面的函数

interface SquareConfig {
  readonly color?: string;
  readonly width?: number;
}

function createSquare(config: SquareConfig = {}): { color: string; area: number } {
  let newSquare = { color: "white", area: 100 };
  if (config.color) {
    config.color = "red"  // error
    newSquare.color = config.color
  }
  if (config.width) {
    config.width = 200  // error
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}

我们将接口中的 color 和 width 都配置成 只读 。所以当在函数体中进行修改参数时会报错。

那我们怎么使用无接口的方法达到同样的效果(配置属性不可修改)?

我们知道原生JS中,每个属性都包含了一个描述符,描述符中有四个配置项,可配置属性行为

  • configurable: 表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
  • enumerable: 表示能否通过for-in循环返回属性。
  • writable: 表示能否修改属性的值。
  • value: 包含这个属性的数据值。

我们利用其中的 writable 是否可以实现呢?

看下面的代码

function createSquare(config: {color?:string,width?:number} = {}): { color: string; area: number } {
  let newSquare = { color: "white", area: 100 };

  Object.defineProperty(config, "color", { writable: false})
  Object.defineProperty(config, "width", { writable: false })
  
  if (config.color) {
    config.color = "red"
    newSquare.color = config.color
  }
  if (config.width) {
    config.width = 200
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}

createSquare({ color: "black" }); // error

createSquare({})  // ok

createSquare()  // ok

代码可正确通过TypeScript的编译,但是在JavaScript代码运行时会出错。(TypeScript在编译时期检查错误,检查的是语法错误。我们写的代码没有语法错误,所以可以通过编译)

可以看到,虽然这里实现了与接口基本相同的效果,但是编译阶段没办法捕捉到错误。

此外还有只读类型数组

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;

a.push(1) // ok
ro.push(2)  // error

let b = ro as Array<number>

b.push(1) // ok

数组设置成只读的之后,将不能再使用原来操作数组的各种方法。但是进行类型断言(类型断言是一种强制转化的机制,TypeScript认为人比程序更了解这个变量是什么类型)之后就又可以了

额外的类型检查

还以上面的代码为例

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): {color: string; area: number} {
  ...
}

let mySquare = createSquare({ height: 22 });  // 报错

向函数中传入了接口规定的属性以外的属性,会报错。这其实很好理解是为什么,没有经过类型检查等手段就将一些变量带入函数的运行环境中,那要 TypeScript 有什么用呢? TypeScript 最大的优点就是将所有变量可以掌控,哪一个阶段数据是什么状态都是明明白白的。

那怎么解决上面的这个问题 ?

可以使用类型断言,将我们传入的参数断言成接口类型的数据

let mySquare = createSquare({ height: 22 } as SquareConfig);  

这里注意为什么将{ height: 22} 转化成 SquareConfig 类型之后就能用了,明明 SquareConfig 中并不存在 height 属性。这是因为类型断言不进行特殊的数据检查和解构

原文中还给出了"字符串索引签名"这种方式

interface SquareConfig {
    color?: string;
    width?: number;
    [propName: string]: any;
}

表示SquareConfig可以有任意数量的属性,并且只要它们不是 color 和 width,那么就无所谓它们的类型是什么

"字符串索引签名"这里先放着,之后再学习

最后也是最不推荐的一种是绕过检查

createSquare({ colour: "red", width: 100 });  // 报错

let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);   // 正确

这里为什么能绕过检查????原文解释是这样的因为 squareOptions不会经过额外属性检查,所以编译器不会报错.这里保留疑问..

请教了大神之后明白了 TypeScript绕过编译器检查的一点困惑

函数类型

interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source, subString) {
  let result = source.search(subString);
  return result > -1;
}

// 代码相当于
let mySearch: (source: string, subString: string) => boolean = function (source, subString) : boolean {
  let result = source.search(subString);
  return result > -1;
}

// (source: string, subString: string) => boolean 也是规范函数写法的一种形式

mySearch函数 是 SearchFunc 类型的,SearchFunc 接口中定义了必须要传入的参数及参数类别,以及返回值的类别。那示例中函数定义中的参数类型指定显得有些多余了,去掉也是没错的

mySearch = function(source, subString) {
  ...
}

另外定义函数的函数参数名称是可以改变的,函数的参数会逐个进行检查,位置对了就行

mySearch = function(sur, sub) {
  ...
}

那这里传入一个需要变量解构的对象,接口该怎么定义?

如下

function test({ sid }) {
  ...
}

test({ sid: '123', num: 123 })

定义如下:

interface Sid {
  sid: string,
  [propName: string]: any;
}

interface Test {
  (sid: Sid, num: number): number
}

const test: Test = function ({ sid }, num) {
  return Number.parseInt(sid) + num
}

test({ sid: '123', ha: '234' }, 2)

可索引的类型

interface StringArray {
  [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

表示了当用 number 去索引 StringArray 时会得到 string 类型的返回值。这就相当于一个字符串数组

这种东西定义成一个数组不就完了?为什么要弄出这种东西?

类类型

实现接口

与C#或Java里接口的基本作用一样,TypeScript也能够用它来明确的强制一个类去符合某种契约

interface ClockInterface {
  currentTime: Date;
  setTime(d: Date): void;
}

class Clock implements ClockInterface {
  currentTime: Date;
  constructor() {
    this.currentTime = new Date()
  }
  setTime(d: Date) {
    this.currentTime = d;
  }
}

接口中描述的数据和方法挂载到原型上

接口中定义的属性将在类中属性上定义。定义的方法将挂载到原型上。

类静态部分与实例部分的区别

// 函数接口
interface ClockConstructor {
    // 定义了一个构造函数,返回 ClockInterface 类型数据
    new (hour: number, minute: number): ClockInterface;
}

// 类接口
interface ClockInterface {
    // 定义了一个继承类需要实现的函数。
    tick():void;
}

// 参数 ctor 为构造函数
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
    return new ctor(hour, minute);
}

// 继承类需要实现 tick 函数
class DigitalClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("beep beep");
    }
}
class AnalogClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("tick tock");
    }
}

let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

两个类均继承自 ClockInterface , 所以他们应当实现接口中的 tick 函数。

观察函数 createClock

参数ctor: ClockConstructor相当于const ctor : ClockConstructor = DigitalClock

于是再看 接口ClockConstructor,接口中表明,实现该接口的函数需要返回 ClockInterface 的一个实例

直接定义一个接口规范类中的静态部分和实例部分不好实现。代码中使用了两个接口实现分别规范这两个部分。

ClockInterface 接口直接规范了实例部分函数

ClockConstructor 接口规范了构造函数,规范了静态部分

继承接口

看代码、很容易明白

interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

interface Square extends Shape, PenStroke {
    sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

混合类型

看代码

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

function getCounter(): Counter {
    let counter = <Counter>function (start: number) { };
    // 相当于 let counter = function (start: number) { } as Counter;
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

接口中定义了三种类型,用在函数上,函数应当包含一个 number 参数,返回 string 类型数据,观察 getCounter 函数,函数内部定义了一个名为 counter 函数,函数被强转为 Counter 类型,所以可以挂载在 Counter 接口中定义的 reset 和 interval

接口继承类

看代码

class Control {
    private state: any;
}

interface SelectableControl extends Control {
    select(): void;
}

class Button extends Control implements SelectableControl {
    select() { }
}

class TextBox extends Control {
    select() { }
}

// 错误:“Image”类型缺少“state”属性。
class Image implements SelectableControl {
    select() { }
}

class Location {

}

原文说到"接口继承类时,会继承其的 private 和 protected 成员"。实际上将上面代码 Control 类中的 private 改为 public ,Image 类也会报错。

先看代码

class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters);
    }
}

class Horse extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 45) {
        console.log("Galloping...");
        super.move(distanceInMeters);
    }
}

let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move();
tom.move(34);

像是 Java 和 ES6 语法的混合。很好看懂

定义到 constructor 中的属性,需要在 class 的大括号内表明是什么类型的

private 修饰符

私有属性修饰符。被修饰属性只能在当前类中被访问。

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

class Rhino extends Animal {
    constructor() { super("Rhino"); }
}

class Employee {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");

animal = rhino;
animal = employee; // 错误: Animal 与 Employee 不兼容.

只有当 私有属性 来源于同一处时,才兼容

protected 修饰符

保护属性修饰符。被修饰属性除了能在当前类被访问,还可以被派生类访问

class Person {
    protected name: string;
    protected constructor(theName: string) { this.name = theName; }
}

// Employee 能够继承 Person
class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
let john = new Person("John"); // 错误: 'Person' 的构造函数是被保护的.

public 修饰符

在哪都能被访问到。没有修饰符时,默认为 public

readonly 修饰符

数据只读。只读属性必须在声明时或构造函数里被初始化

get/set 存取器

简单看个例子就好

let passcode = "secret passcode";

class Employee {
    private _fullName: string;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        if (passcode && passcode == "secret passcode") {
            this._fullName = newName;
        }
        else {
            console.log("Error: Unauthorized update of employee!");
        }
    }
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    alert(employee.fullName);
}

静态属性

静态属性存在于类本身上面而不是类的实例上。

class Grid {
    static origin = {x: 0, y: 0};
    calculateDistanceFromOrigin(point: {x: number; y: number;}) {
        let xDist = (point.x - Grid.origin.x);
        let yDist = (point.y - Grid.origin.y);
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }
    constructor (public scale: number) { }
}

let grid1 = new Grid(1.0);  // 1x scale
let grid2 = new Grid(5.0);  // 5x scale

console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

改写成我们常见的 JavaScript 形式

function Grid (scale) {
  this.scale = scale
}

Grid.origin = { x:0, y:0} // 这个就是 TypeScript 中的静态属性

Grid.prototype.calculateDistanceFromOrigin = function (point) {
  ...
}

抽象类

抽象类做为其它派生类的基类使用

abstract class Department {

    constructor(public name: string) {
    }

    printName(): void {
        console.log('Department name: ' + this.name);
    }

    abstract printMeeting(): void; // 必须在派生类中实现
}

class AccountingDepartment extends Department {

    constructor() {
        super('Accounting and Auditing'); // 在派生类的构造函数中必须调用 super()
    }

    printMeeting(): void {
        console.log('The Accounting Department meets each Monday at 10am.');
    }

    generateReports(): void {
        console.log('Generating accounting reports...');
    }
}

let department: Department; // 允许创建一个对抽象类型的引用
department = new Department(); // 错误: 不能创建一个抽象类的实例
department = new AccountingDepartment(); // 允许对一个抽象子类进行实例化和赋值
department.printName();
department.printMeeting();
department.generateReports(); // 错误: 方法在声明的抽象类中不存在

抽象类中的抽象方法不包含具体实现并且必须在派生类中实现.相对于接口中定义的方法,抽象类定义抽相关方法,需要加 abstract 关键字。

接口与抽象类的区别:接口中的定义必须在其继承中进行实现,接口中本身不包含实现,只有定义。抽象类中的普通函数需要具体实现,抽象函数则在派生各类中进行实现。

函数

let myAdd: (x: number, y: number) => number = function (x: number, y: number): number { return x + y; };

看教程中给出的这个实例,不是特别容易看懂

以等号为分界线。左边定义了一个变量 myAdd , 冒号后表明了类型:两个 number 参数和返回 number 类型数据

在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。

完整的一个函数应当是上面这种定义,但可以省略部分

代码如下:

// 由右边可以推断左边
let myAdd = function(x: number, y: number): number { return x + y; };

// 由左边可以推断右边
let myAdd: (baseValue: number, increment: number) => number =
    function(x, y) { return x + y; };

剩余参数

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}

let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;

注意与 ES6 区别在于需要规范 数组类型

this

直接调用 this 时注意 this 为 any 类型。需要手动指定一下 this 的类型

实例如下

interface Card {
    suit: string;
    card: number;
}
interface Deck {
    suits: string[];
    cards: number[];
    createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    // NOTE: The function now explicitly specifies that its callee must be of type Deck
    createCardPicker: function(this: Deck) {
        return () => {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);

            return {suit: this.suits[pickedSuit], card: pickedCard % 13};
        }
    }
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

文档中说直接调用 this 会报错,但是我在实际测试中并没有发现这个问题...实例代码是处理 this 之后的代码。可以 看到手动指定了一波this是什么

函数重载

传统的面向对象编程语言中,重载概念是:在函数名相同的情况下,根据参数个数、参数类型不同的情况下调用不同的函数。

但在 JavaScript 中,函数名相同的话,会取最后定义的函数。

TypeScript 中函数重载是 申明了相同函数名称,只是参数类型、个数、返回类型不同。具体函数实现还是只有一个。

let suits = ["hearts", "spades", "clubs", "diamonds"];

// 定义了两种函数
// {suit: string; card: number; }[] 表示数组每一项都是 包含 suit 和 card 的对象
function pickCard(x: {suit: string; card: number; }[]): number;

function pickCard(x: number): {suit: string; card: number; };

// 函数具体实现
function pickCard(x): any {

    if (typeof x == "object") {
        let pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    }

    else if (typeof x == "number") {
        let pickedSuit = Math.floor(x / 13);
        return { suit: suits[pickedSuit], card: x % 13 };
    }
}

let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];

alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

泛型

泛型变量

先看例子

function Test( ary: any ): any {
  return ary
}

这个函数隐藏了一些信息。我们传入一些参数,并不知道函数会返回哪些类型的数据。

泛型就是解决这个问题的

再看用泛型改写之后的例子

function Test<T> (ary: T): T {
  return ary
}

T不指代具体的类型,只表明类型一致。

函数调用示例

const t = Test<string>("abcd")

// 省略写法如下
const t2 = Test("abcd")

相较于 any, 利用泛型可以明确返回什么类型数据

加上 <> 之后,很容易和 类型断言 混了,所以平时用 类型断言 时,用 as 形式

泛型接口

// 代码1
interface GenericIdentityFn {
    <T>(arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn = identity;
myIdentity(2)

// 代码2
interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;
myIdentity(2)

代码1 中可以根据传入的参数自动推断出 T 是什么类型的

代码2 相较于代码1 泛型参数当作整个接口的一个参数。在定义 myIdentity 时就指定 T 是什么类型,相对来说 代码2更清晰

泛型类

原文给出的代码并不能正确运行,我添加了一些内容

const parm = '123'
class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
  static n: T = <T>(parm)   // 报错
  constructor ( n: T) {
    this.zeroValue = n
    this.add = function (x, y) {
      return <T>(x)
    }
  }
}

let myGenericNumber = new GenericNumber<number>(1);
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) { return x + y; };

其实到这里就可以看出,泛型其实就是统一类型用的。

泛型类指的是实例部分的类型,类的静态属性不能使用泛型类型

泛型约束

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}

如代码所示。泛型约束规范传入的 T 类型具有什么样的特征。示例代码需要传入具有 length 属性的类型数据

在泛型中使用类类型

看代码

class BeeKeeper {
    hasMask: boolean;
}

class ZooKeeper {
    nametag: string;
}

class Animal {
    numLegs: number;
}

class Bee extends Animal {
    keeper: BeeKeeper;
}

class Lion extends Animal {
    keeper: ZooKeeper;
}

function createInstance<A extends Animal>(c: new () => A): A {
    return new c();
}

createInstance(Lion).keeper.nametag;  // typechecks!
createInstance(Bee).keeper.hasMask;   // typechecks!

首先代码定义了5个类,类中分别定义了属性(示例代码不完整,缺 constructor).

观察 Bee 和 Lion,可以发现两者均继承自 Animal 类,由此,二者均可以调用 Animal 类中的公有属性 numLegs。再看 Bee 中的 keep , 数据类型为 BeeKeeper, 由此 由Bee 创建的实例可以通过 keeper 来调用 BeeKeeper 中的数据。Lion同理

再看 createInstance 函数,使用泛型约束 传入的类型必须具有 Animal 类中的 numLegs 属性。因为 Bee 和 Lion 均继承自 Animal,所有都有这个属性。看函数参数c是一个 A类型的构造函数

枚举

基础类型 中简单介绍了枚举类型。

看代码

enum Color {
  Green,
  Blue,
  Red
}

使用 enum 关键字定义一个 枚举类型 数据结构。

使用示例:

const red: string = Color[2]

const color: number = Color.Red

如上的定义 Color 代码,默认使用 0,1,2...递增编号。也可以手动指定编号

如下

enum Color {
  Green= 2,
  Blue= 4,
  Red
}

原文代码中给出这样一个例子

enum Response {
  No = 0,
  Yes = 1,
}

function respond(recipient: string, message: Response): void {
  // ...
}

respond("Princess Caroline", Response.Yes)

尝试将函数参数中的 Response 改为 number, 结果还是正确的

const message: Response = 1

这就说明数字类型与枚举类型兼容

字符串枚举

上面是使用数字编号进行引索,实际还可以使用字符串枚举

enum Color {
  Green='This is green',
  Blue='This is blue',
  Red='this is red'
}

计算的和常量成员

enum FileAccess {
    None,
    Read = 1 << 1,
    Write = 1 << 2,
    ReadWrite  = Read | Write,
    G = "123".length
}

<<是 js 中的位移运算符, | 是或运算符

类型推论

最佳通用类型

看下面的例子

let x = [1, '2']  // 正确

x = [true, '3'] // 错误

定义时没有类型,编译器会自动推断 x 是什么类型。

代码中 x 被推断为 元组 [number, string], 所以 x 赋值 [true, '3'] 失败

再看这个

let zoo = [new Rhino(), new Elephant(), new Snake()];

会被推断为联合数组类型

相当于

let zoo:(Rhino, Elephant, Snake)[] = [new Rhino(), new Elephant(), new Snake()];

类型兼容性

TypeScript里的类型兼容性是基于结构子类型的。 结构类型是一种只使用其成员来描述类型的方式。 它正好与名义(nominal)类型形成对比。在基于名义类型的类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。这与结构性类型系统不同,它是基于类型的组成结构,且不要求明确地声明。

还是给一些实例代码自己看

// 案例1
interface Named {
    name: string;
}

class Person {
    name: string;
}

let p: Named;
p = new Person(); // 正确

// 案例2
interface Named {
    name: string;
}

let x: Named;
let y = { name: 'Alice', location: 'Seattle' };
x = y;

// 案例3
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // 正确
x = y; // 错误

// 案例4
interface Named {
    name: string;
}

class Person {
    name: number;
}

let p: Named;
p = new Person(); // 错误

案例1 中 Named 接口与 Person 类具有相同的组成结构,所以允许赋值

案例2 中 { name: 'Alice', location: 'Seattle' }中包含了 x 中需要的 name 属性,所以允许赋值

案例3 中 函数参数 比较二者对应位置的参数类型是否相同。

x 函数的参数类型都能在 y 函数中找到对应类型。所以可以兼容。这里注意与 案例2 的区别,案例2 是右边多,但桉树这里是左边多

案例4 中 虽然二者都有 name 属性,但类型不一致,所以不兼容

枚举

枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。

类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。

实例部分是只有在创建对象时,才初始化的数据,静态部分就是将属性直接挂在到构造函数上(在 TypeScript 中静态部分使用关键字 static 标识)。

class Animal{
  name: string
  static age: number = 12
  constructor (n: string) {
    this.name = n
  }
  setName (name: string) {
    this.name = name
  }
}

class Person{
  name: string
  static aged: number = 100
  constructor(n: string, age?: number ) {
    this.name = n
  }
  setName (name: string) {
    this.name = name
  }
}

let an: Animal = new Animal('1')
let pn: Person = new Person('2')

pn = an // 正确

注意上面代码中 name 属性实际是 public 类型,当改为 private 或者 protected 时就报错了。但如果 Animal 和 Person 类中的 name 均继承自父类,那么就可以又兼容了。

高级类型

交叉类型

这种类型实际中没见过用。。这里还是放一下代码,自己看

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

看一下还是很容易理解的

用 ES6 改写一下

class Person {
  constructor (name) {
    this.name = name
  }
}

class ConsoleLogger {
  log () {
    // ...
  }
}

function extend (obj1, obj2) {
  const result = {}
  for (let i in obj1) {
    result[i] = obj1[i]
  }
  for (let i in obj2) {
    result[i] = obj2[i]
  }
  return result
}

可以看到,代码实际在 extend 函数中创建了一个对象,该对象集合了 obj1,obj2 中所有可遍历的属性

联合类型

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

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

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

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

let pet = getSmallPet();
pet.layEggs(); // 正确
pet.swim();    // 失败

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

类型保护与区分类型

看示例代码

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

用户自定义的类型保护

如何在类型检查后,能够清楚的知道是什么类型呢?

看下面代码

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

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

pet is Fish就是类型谓词。谓词为 parameterName is Type这种形式

如上代码只能识别 Fish, 如果要识别 Bird 还需要写个函数

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

这样代码量增加了好多。于是有了下面的 typeof 类型 保护

typeof 类型保护

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

"typename"必须是 "number", "string", "boolean"或 "symbol"

instanceof 类型保护

与 typeof 相似

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

类型别名

类型别名会给一个类型起个新名字
看例子

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
type Container<T> = { value: T };
type Tree<T> = {
    value: T;
    left?: Tree<T>;
    right?: Tree<T>;
}

type Easing = "ease-in" | "ease-out" | "ease-in-out"

与接口区别:

1、类型别名不能被 extends和 implements

2、接口创建了一个新的名字,可以在其它任何地方使用,类型别名并不创建新名字

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

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

索引类型

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 索引类型查询操作符。对于任何类型 T, keyof T的结果为 T上已知的公共属性名的联合

例如:

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

映射类型

用处在于 将一个已知的类型每个属性都变为可选的

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