TypeScript教程

203 阅读31分钟

参考网址:www.tslang.cn/docs/handbo…

TypeScript 是一门基于 JavaScript 拓展的语言,它是 JavaScript 的超集,并且给 JavaScript 添加了静态类型检查系统。TypeScript 能让我们在开发时发现程序中类型定义不一致的地方,及时消除隐藏的风险,大大增强了代码的可读性以及可维护性。

TS和JS

编译

1. JavaScript运行依赖NodeJs环境和浏览器环境

www.cnblogs.com/raind/p/105…

javascript是一种解释型,直译式脚本语言,JavaScript 本身是没有预编译的,浏览器内的js引擎直接解释源代码并执行。在浏览器运行 JavaScript 之前必须编译成客户端的机器码。

  • JavaScript代码转换为JavaScript-AST
  • AST代码转换为字节码
  • 运算时计算字节码

2. TypeScript运行流程,以下操作均为TSC操作,三步执行完继续同上操作,让浏览器解析

zhuanlan.zhihu.com/p/45898674

  • TypeScript代码编译为 TypeScript-AST
  • 检查AST代码上类型检查
  • 类型检查后,编译为JavaScript代码
  • JavaScript代码转换为JavaScript-AST
  • AST代码转换为字节码
  • 运算时计算字节码

preview

1.预处理器(preprocessing)处理

预处理器负责根据待编译文件计算参与编译的文件,生成源文件列表,构成编译上下文Program

img

编译列表中的文件 = 待编译文件 + 依赖文件 + @types 文件

待编译文件:默认为项目目录下所有的 .ts、.tsx、.d.ts 为待编译文件(tsconfig.json)

依赖文件 :

  1. <reference path=... /> 标签引入的依赖声明文件
  2. import 表达式引入的文件

注意: 当解析 import 导入的的时候,会优先选择 .ts/.tsx文件而不是 .d.ts 文件,以确保处理的是最新的文件

@types:

所有可见的 @types 目录下的所有文件

如:node_modules/@types./node_modules/@types/等等

2.语法分析器(parser)处理

语法分析器将预处理器得到的源文件列表中的文件解析生成包含抽象语法树(AST)Node 的 SourceFile 对象

SourceFile对象 = 源文件 AST + 额外信息 (如文件名及文件信息等)

blog.csdn.net/qq_41257129…

类似:

var myDiv = React.createElement('div', { title: 'this is a div', id: 'mydiv' }, '这是一个div', myH1)

3.联合器(Binder)处理

联合器遍历并处理语法分析器生成的 AST,并将 AST 中的声明结合放到一个 Symbol 中。

然后通过 createSourceFile API 生成带有 SymbolSourceFile

SourceFile对象 = 源文件 AST + Symbol + 额外信息 (如文件名及文件信息等)

此时的 Symobl 仅表示单个文件的声明信息

4.、类型解析器与检查器(Type resolver / Checker)处理

4.1、生成 Program

通过调用 createProgramAPI 来创建 Program

Program = All SourceFile + CompilerOptions

4.2、生成 TypeChecker 进行处理

通过 Program 实例创建 TypeChecker

TypeChecker是TypeScript类型系统的核心,它负责计算出不同文件里的Symbols之间的关系,将Type赋值给Symbol,并生成任何语义Diagnostic(比如:error)

处理内容:

  1. TypeChecker 合并不同的 SourceFile 里的 Symbol 到一个单独的视图,创建单一的Symbol表(囊括所有文件的全局Symbol视图 )

  2. 类型检查

    Symbol 合并到一张表后,TypeChecker就可以解决关于这个程序的任何问题了。 这些“问题”可以是:

    1. 这个Node的Symbol是什么?
    2. 这个Symbol的Type是什么?
    3. 在AST的某个部分里有哪些Symbol是可见的?
    4. 某个函数声明的Signature都有哪些?
    5. 针对某个文件应该报哪些错误?

5.生成器(Emitter)处理

通过 Program 创建一个 Emitter

Emitter 将给定的 SourceFile 生成编译后文件(.js.jsx.d.ts.js.map

类型绑定

JavaScript

JavaScript 是一门解释型语言,没有编译阶段,所以它是动态类型。JavaScript动态绑定类型,只有运行程序才能知道类型,在程序运行之前JavaScript对类型一无所知

TypeScript

TypeScript是在程序运行前(也就是编译时)就会知道当前是什么类型。当然如果该变量没有定义类型,那么TypeScript会自动类型推导出来。

类型转换

JavaScript弱类型语言

比如在JavaScript1 + true这样一个代码片段,JavaScript存在隐式转换,这时true会变成number类型number(true)和1相加。

TypeScript强类型语言

TypeScript中,1+true这样的代码会在TypeScript中报错,提示number类型不能和boolean类型进行运算。

何时检查类型

JavaScript

JavaScript中只有在程序运行时才能检查类型。类型也会存在隐式转换,很坑。

TypeScript

TypeScript中,在编译时就会检查类型,如果和预期的类型不符合直接会在编辑器里报错、爆红

TS特殊符号

blog.csdn.net/qiwoo_weekl…

?. 运算符

可选链(Optional Chaining)运算符是一种先检查属性是否存在,再尝试访问该属性的运算符

a?.b;
// 相当于 a == null ? undefined : a.b;
// 如果 a 是 null/undefined,那么返回 undefined,否则返回 a.b 的值.

a?.[x];
// 相当于 a == null ? undefined : a[x];
// 如果 a 是 null/undefined,那么返回 undefined,否则返回 a[x] 的值

a?.b();
// 相当于a == null ? undefined : a.b();
// 如果 a 是 null/undefined,那么返回 undefined
// 如果 a.b 不是函数的话,会抛类型错误异常,否则计算 a.b() 的结果

?:

在 TypeScript 中使用 interface 关键字就可以声明一个接口:

interface Person {
  name: string;
  age: number;
}
let semlinker: Person = {
  name: "semlinker",
  age: 33,
};

在以上代码中,我们声明了 Person 接口,它包含了两个必填的属性 nameage。在初始化 Person 类型变量时,如果缺少某个属性,TypeScript 编译器就会提示相应的错误信息,比如:

// Property 'age' is missing in type '{ name: string; }' but required in type 'Person'.(2741)
let lolo: Person  = { // Error
  name: "lolo"  
}

为了解决上述的问题,我们可以把某个属性声明为可选的:

interface Person {
  name: string;
  age?: number;
}
​
let lolo: Person  = {
  name: "lolo"  
}

| 分隔符

在 TypeScript 中联合类型(Union Types)表示取值可以为多种类型中的一种,联合类型使用 | 分隔每个类型。联合类型通常与 nullundefined 一起使用:

const sayHello = (name: string | undefined) => { /* ... */ };

以上示例中 name 的类型是 string | undefined 意味着可以将 stringundefined 的值传递给 sayHello 函数。

type

定义类型由两种方式:接口(interface)和类型别名(type alias)

interface只能定义对象类型,type声明的方式可以定义组合类型,交叉类型和原始类型

基础类型

布尔值

最基本的数据类型就是简单的true/false值,在JavaScript和TypeScript里叫做boolean(其它语言中也一样)。

let isDone: boolean = false;

数字

和JavaScript一样,TypeScript里的所有数字都是浮点数。 这些浮点数的类型是 number。 除了支持十进制和十六进制字面量,TypeScript还支持ECMAScript 2015中引入的二进制和八进制字面量。

let decLiteral: number = 6;// 十进制
let hexLiteral: number = 0xf00d; // 十六进制
let binaryLiteral: number = 0b1010;// 二进制
let octalLiteral: number = 0o744;  // 八进制

字符串

let name: string = "bob";
name = "smith";

你还可以使用模版字符串,它可以定义多行文本和内嵌表达式。 这种字符串是被反引号包围( ``),并且以${ expr }`这种形式嵌入表达式

let name: string = `Gene`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ name }.I'll be ${ age + 1 } years old next month.`;

这与下面定义sentence的方式效果相同:

let sentence: string = "Hello, my name is " + name + ".\n\n" +"I'll be " + (age + 1) + " years old next month.";

Void空值

某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void

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

声明一个void类型的变量没有什么大用,因为你只能为它赋予undefinednull

let unusable: void = undefined;

Null 和 Undefined

null

null是一个只有一个值的特殊类型。表示一个空对象引用。在 JavaScript 中 null 表示 "什么都没有"。用 typeof 检测 null 返回是 object。

undefined

在 JavaScript 中, undefined 是一个没有设置值的变量。typeof 一个没有值的变量会返回 undefined。

void 的区别是,undefinednull 是所有类型的子类型。也就是说 undefined 类型的变量,可以赋值给 number 类型的变量:

// 这样不会报错
let num: number = undefined;
// 这样也不会报错
let u: undefined;
let num: number = u;

void 类型的变量不能赋值给 number 类型的变量:

let u: void;
let num: number = u;

// Type 'void' is not assignable to type 'number'.

数组

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

let list: number[] = [1, 2, 3];

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

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

元组 Tuple

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为 stringnumber类型的元组。

// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error

当访问一个已知索引的元素,会得到正确的类型:

console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'

当访问一个越界的元素,会使用联合类型替代:

x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型

console.log(x[5].toString()); // OK, 'string''number' 都有 toString

x[6] = true; // Error, 布尔不是(string | number)类型

Object

object表示非原始类型,也就是除numberstringbooleansymbolnullundefined之外的类型。

使用object类型,就可以更好的表示像Object.create这样的API。例如:

declare function create(o: object | null): void;

create({ prop: 0 }); // OK
create(null); // OK

create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error

Any

有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any类型来标记这些变量:

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

在对现有代码进行改写的时候,any类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。

let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)

let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.

当你只知道一部分数据的类型时,any类型也是有用的。 比如,你有一个数组,它包含了不同的类型的数据:

let list: any[] = [1, true, "free"];

list[1] = 100;

变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型

let something;
something = 'seven';
something = 7;

something.setName('Tom');

等价于

let something: any;
something = 'seven';
something = 7;

something.setName('Tom');

Never

never 是其它类型(包括 null 和 undefined)的子类型,代表那些永不存在的值的类型。这意味着声明为 never 类型的变量只能被 never 类型所赋值。 例如, never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型;

let x: never;
let y: number;
​
// 运行错误,数字类型不能转为 never 类型
x = 123;
​
// 运行正确,never 类型可以赋值给 never类型
x = (()=>{ throw new Error('exception')})();
​
// 运行正确,never 类型可以赋值给 数字类型
y = (()=>{ throw new Error('exception')})();
​
// 返回值为 never 的函数可以是抛出异常的情况
function error(message: string): never {
    throw new Error(message);
}
​
// 返回值为 never 的函数可以是无法被执行到的终止点的情况
function loop(): never {
    while (true) {}
}

类型断言

类型断言(Type Assertion)可以用来手动指定一个值的类型。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 它没有运行时的影响,只是在编译阶段起作用。 类型断言会假设程序员,已经进行了必须的检查,让TypeScript跳过类型检测。

语法:<类型>值 或 值 as 类型
在 tsx 语法(React 的 jsx 语法的 ts 版)中必须用后一种

类型断言有两种形式。 其一是“尖括号”语法:

let someValue: any = "this is a string";
​
let strLength: number = (<string>someValue).length;

另一个为as语法:

let someValue: any = "this is a string";
​
let strLength: number = (someValue as string).length;
  • 例子一

    function func(val: string | number): number {
      if (val.length) {
        return val.length
      } else {
        return val.toString().length
      }
    }
    

    函数的参数 val 是一个联合类型,在这里的意思是说 val 可以是字符串类型也可以是数值类型。代码中要返回参数的长度,但是 length 是字符串的属性,而数值是没有这个属性的,所以当 val 是数值时,就先用 toSting() 来将数字转换为字符串再取长度。这样的逻辑本身没问题,但是在编译阶段一访问 val.length 时就报错了,因为 访问联合类型值的属性时,这个属性必须是所有可能类型的共有属性, 而length不是共有属性,val 的类型此时也没确定,所以编译不通过。为了通过编译,此时就可以使用类型断言了

    function func(val: string | number): number {
      if ((<string>val).length) {
        return (<string>val).length
      } else {
        return val.toString().length
      }
    }
    
  • 例子二

    const foo = {};
    foo.bar = 123; // Error: 'bar' 属性不存在于 ‘{}’
    foo.bas = 'hello'; // Error: 'bas' 属性不存在于 '{}'
    ​
    interface Foo {
      bar: number;
      bas: string;
    }
    const foo = {} as Foo;
    foo.bar = 123;
    foo.bas = 'hello';
    

const 断言

TypeScript 3.4 引入了一种新的字面量构造方式,也称为 const 断言。当我们使用 const 断言构造新的字面量表达式时,我们可以向编程语言发出以下信号

  • 表达式中的任何字面量类型都不应该被扩展;
  • 对象字面量的属性,将使用 readonly 修饰;
  • 数组字面量将变成 readonly 元组。

下面我们来举一个 const 断言的例子:

let x = "hello" as const;
type X = typeof x; // type X = "hello"
let y:X='hello';
​
let y = [10, 20] as const;
type Y = typeof y; // type Y = readonly [10, 20]
​
let z = { text: "hello" } as const;
type Z = typeof z; // let z: { readonly text: "hello"; }

类型断言的用途

  • 将一个联合类型断言为其中一个类型
  • 将一个父类断言为更加具体的子类
  • 将任何一个类型断言为 any
  • any 断言为一个具体的类型

类型推论

如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。

以下代码虽然没有指定类型,但是会在编译的时候报错:

let myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
​
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

事实上,它等价于:

let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;
​
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。

如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查

let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

枚举

是什么

枚举是一个被命名的整型常数的集合,用于声明一组命名的常数,当一个变量有几种可能的取值时,可以将它定义为枚举类型。

通俗来说,枚举就是一个对象的所有可能取值的集合。

在日常生活中也很常见,例如表示星期的SUNDAY、MONDAY、TUESDAY、WEDNESDAY、THURSDAY、FRIDAY、SATURDAY就可以看成是一个枚举。

枚举的说明与结构和联合相似,其形式为:

enum 枚举名{
    标识符①[=整型常数],
    标识符②[=整型常数],
    ...
    标识符N[=整型常数],
}枚举变量;

使用

枚举的使用是通过enum关键字进行定义,形式如下:

enum xxx { ... }

声明关键字为枚举类型的方式如下:

// 声明d为枚举类型Direction
let d: Direction;

类型可以分成:

  • 数字枚举
  • 字符串枚举
  • 异构枚举

数字枚举

当我们声明一个枚举类型是,虽然没有给它们赋值,但是它们的值其实是默认的数字类型,而且默认从0开始依次累加:

enum Direction {
    Up,   // 值默认为 0
    Down, // 值默认为 1
    Left, // 值默认为 2
    Right // 值默认为 3
}
​
console.log(Direction.Up === 0); // true
console.log(Direction.Down === 1); // true
console.log(Direction.Left === 2); // true
console.log(Direction.Right === 3); // true

如果我们将第一个值进行赋值后,后面的值也会根据前一个值进行累加1:

enum Direction {
    Up = 10,
    Down,
    Left,
    Right
}
​
console.log(Direction.Up, Direction.Down, Direction.Left, Direction.Right); // 10 11 12 13

字符串枚举

枚举类型的值其实也可以是字符串类型:
​
enum Direction {
    Up = 'Up',
    Down = 'Down',
    Left = 'Left',
    Right = 'Right'
}
​
console.log(Direction['Right'], Direction.Up); // Right Up

如果设定了一个变量为字符串之后,后续的字段也需要赋值字符串,否则报错:

enum Direction {
 Up = 'UP',
 Down, // error TS1061: Enum member must have initializer
 Left, // error TS1061: Enum member must have initializer
 Right // error TS1061: Enum member must have initializer
}

异构枚举

即将数字枚举和字符串枚举结合起来混合起来使用,如下:

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}

通常情况下我们很少会使用异构枚举

本质

现在一个枚举的案例如下:

enum Direction {
    Up,
    Down,
    Left,
    Right
}

通过编译后,javascript如下:

var Direction;
(function (Direction) {
    Direction[Direction["Up"] = 0] = "Up";
    Direction[Direction["Down"] = 1] = "Down";
    Direction[Direction["Left"] = 2] = "Left";
    Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));

上述代码可以看到, Direction[Direction["Up"] = 0] = "Up"可以分成

  • Direction["Up"] = 0
  • Direction[0] = "Up"

所以定义枚举类型后,可以通过正反映射拿到对应的值,如下:

enum Direction {
    Up,
    Down,
    Left,
    Right
}
​
console.log(Direction.Up === 0); // true
console.log(Direction[0]); // Up

并且多处定义的枚举是可以进行合并操作,如下:

enum Direction {
    Up = 'Up',
    Down = 'Down',
    Left = 'Left',
    Right = 'Right'
}
​
enum Direction {
    Center = 1
}

编译后,js代码如下:

var Direction;
(function (Direction) {
    Direction["Up"] = "Up";
    Direction["Down"] = "Down";
    Direction["Left"] = "Left";
    Direction["Right"] = "Right";
})(Direction || (Direction = {}));
(function (Direction) {
    Direction[Direction["Center"] = 1] = "Center";
})(Direction || (Direction = {}));

可以看到,Direction对象属性回叠加

{ 
  1: 'Center',
  Up: 'Up',
  Down: 'Down',
  Left: 'Left',
  Right: 'Right',
  Center: 1 
}

应用场景

就拿回生活的例子,后端返回的字段使用 0 - 6 标记对应的日期,这时候就可以使用枚举可提高代码可读性,如下:

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
​
console.log(Days["Sun"] === 0); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true

包括后端日常返回0、1 等等状态的时候,我们都可以通过枚举去定义,这样可以提高代码的可读性,便于后续的维护

/**
 * 投放方式
 * 0: 无对接投放
 * 1: 直接投放
 * 2: 事件触发投放
 */
export enum PUT_WAY {
  NO_DOCKING = 0,
  DIRECT = 1,
  EVENT = 2
}
​
export const PUT_WAY_LABEL = {
  [`${PUT_WAY.NO_DOCKING}`]: '无对接投放',
  [`${PUT_WAY.DIRECT}`]: '直接投放',
  [`${PUT_WAY.EVENT}`]: '事件触发投放'
}

接口

作用:

  • 对“对象”进行约束描述
  • 对“类”的一部分行为进行抽象

特点:

  • 接口与值的结构必须相同

    interface LabelledValue {
        size: number;
        label: string;
    }
    function printLabel(labelledObj: LabelledValue) {
        console.log(labelledObj.label);
    }
    let myObj1 = { label: "Size 10 Object" };
    let myObj2 = { label: "Size 10 Object", other:1 };
    //少了
    printLabel(myObj1);
    //多了
    printLabel(myObj2);
    
  • 可以捕获引用了不存在的属性时的错误

    interface LabelledValue {
        size: number;
        label: string;
    }
    function printLabel(labelledObj: LabelledValue) {
        console.log(labelledObj.labell);
      }
    let myObj = { size:10, label: "Size 10 Object" };
    printLabel(myObj);
    

可选属性

接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 可选属性在应用“option bags”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。

下面是应用了“option bags”的例子:

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

带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?符号。

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly来指定只读属性:

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

你可以通过赋值一个对象字面量来构造一个Point。 赋值后, xy再也不能被改变了。

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

TypeScript具有ReadonlyArray<T>类型,它与Array<T>相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // 类型“readonly number[]”中的索引签名仅允许读取
ro.push(5); // 类型“readonly number[]”上不存在属性“push”
ro.length = 100; // 无法分配到 "length" ,因为它是只读属性
a = ro; // 类型 "readonly number[]" 为 "readonly",不能分配给可变类型 "number[]"

上面代码的最后一行,可以看到就算把整个ReadonlyArray赋值到一个普通数组也是不可以的。 但是你可以用类型断言重写:

a = ro as number[];

readonly vs const

最简单判断该用readonly还是const的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const,若做为属性则使用readonly

额外的属性检查

interface SquareConfig {
    color?: string;
    width?: number;
}
​
function createSquare(config: SquareConfig): { color: string; width: number } {
  return {color: '1', width: 1}
}
​
let mySquare = createSquare({ colour: "red", width: 100 });

注意传入createSquare的参数拼写为*colour*而不是color。 在JavaScript里,这会默默地失败。 编译器能够捕获引用了未声明属性的错误

// error: 'colour' not expected in type 'SquareConfig'
let mySquare = createSquare({ colour: "red", width: 100 });

解决

  • 最简便的方法是使用类型断言
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
  • 加上额外属性声明,允许对象引用除可选属性以外的其他属性
interface SquareConfig {
    color?: string;
    width?: number;
    [propName: string]: any;//加上额外属性声明,允许对象引用除可选属性以外的其他属性
}

函数类型

为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 具体的格式是一个只有参数列表返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。

interface Func {
  (param1: string, param2: number): boolean;
}

这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。 下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。

const myFunc: Func = function(param1, param2){
  return typeof param1 === "string" && typeof param2 === "number";
};

对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。 比如,我们使用下面的代码重写上面的例子:

interface Func {
  (param1: string, param2: number): boolean;
}
​
const myFunc: Func = function(param2, param3){
  console.log(param2, param3);// 1,2
  return typeof param2 === "string" && typeof param3 === "number";
};
​
myFunc('1',2)

可索引的类型

与使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到”的类型,比如a[10]ageMap["daniel"]。 可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。

// 数字索引——约束数组
// index 是随便取的名字,可以任意取名
// 只要 index 的类型是 number,那么值的类型必须是 string
interface StringArray {
  // key 的类型为 number ,一般都代表是数组
  // 限制 value 的类型为 string
  [index:number]:string
}
let arr:StringArray = ['aaa','bbb'];
console.log(arr);
​
​
// 字符串索引——约束对象
// 只要 index 的类型是 string,那么值的类型必须是 string
interface StringObject {
  // key 的类型为 string ,一般都代表是对象
  // 限制 value 的类型为 string
  [index:string]:string
}
let obj:StringObject = {name:'ccc'};
​

字符串索引签名能够很好的描述dictionary模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了 obj.propertyobj["property"]两种形式都可以。 下面的例子里, name的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:

interface NumberDictionary {
  [index: string]: number;
  length: number;    // 可以,length是number类型
  name: string       // 错误,`name`的类型与索引类型返回值的类型不匹配
}

最后,你可以将索引签名设置为只读,这样就防止了给索引赋值:

interface ReadonlyStringArray {
    readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!

你不能设置myArray[2],因为索引签名是只读的。

类类型

接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。

类实现接口

实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements 关键字来实现。这个特性大大提高了面向对象的灵活性。

举例来说,门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它:

interface Alarm {
    alert(): void;
}
​
class Door {
}
​
class SecurityDoor extends Door implements Alarm {
    alert() {
        console.log('SecurityDoor alert');
    }
}
​
class Car implements Alarm {
    alert() {
        console.log('Car alert');
    }
}
let a = new SecurityDoor()
a.alert()
let b = new Car()
b.alert()

一个类可以实现多个接口,下例中,Car 实现了 AlarmLight 接口,既能报警,也能开关车灯:

interface Alarm {
    alert();
}
​
interface Light {
    lightOn();
    lightOff();
}
​
class Car implements Alarm, Light {
    alert() {
        console.log('Car alert');
    }
    lightOn() {
        console.log('Car light on');
    }
    lightOff() {
        console.log('Car light off');
    }
}

接口继承接口

接口可以继承接口,下例中,我们使用 extends 使 LightableAlarm 继承 Alarm:

interface Alarm {
  alert();
}
​
interface LightableAlarm extends Alarm {
  lightOn()
  lightOff()
}
​
class A implements LightableAlarm {
  alert() {
    console.log(1);
  }
​
  lightOn() {
    console.log(2);
  }
​
  lightOff() {
    console.log(3);
  }
}
​
let c = new A();

接口继承类

接口也可以继承类:

class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
    point(){
       
    }
}
​
interface Point3d extends Point {
    z: number;
}
​
let point3d: Point3d = {x: 1, y: 2, z: 3, point:function(){
    console.log(this.x)
}};
point3d.point()

为什么 TypeScript 会支持接口继承类呢?

实际上,当我们在声明 class Point 时,除了会创建一个名为 Point 的类之外,同时也创建了一个名为 Point 的类型(实例的类型)。

所以我们既可以将 Point 当做一个类来用(使用 new Point 创建它的实例),也可以将 Point 当做一个类型来用(使用 : Point 表示参数的类型)

继承接口

和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

interface Shape {
    color: string;
}
​
interface SquareSquare extends Shape {
    sideLength: number;
}
​
const square ={} as SquareSquare;
square.color = "blue";
square.sideLength = 10;

一个接口可以继承多个接口,创建出多个接口的合成接口。

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;

传统方法中,JavaScript 通过构造函数实现类的概念,通过原型链实现继承。而在 ES6 中,我们终于迎来了 class

TypeScript 除了实现了所有 ES6 中的类的功能以外,还添加了一些新的用法。

这一节主要介绍类的用法,下一节再介绍如何定义类的类型。

类的概念

虽然 JavaScript 中有类的概念,但是可能大多数 JavaScript 程序员并不是非常熟悉类,这里对类相关的概念做一个简单的介绍。

  • 类(Class):定义了一件事物的抽象特点,包含它的属性和方法
  • 对象(Object):类的实例,通过 new 生成
  • 面向对象(OOP)的三大特性:封装、继承、多态
  • 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
  • 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
  • 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 CatDog 都继承自 Animal,但是分别实现了自己的 eat 方法。此时针对某一个实例,我们无需了解它是 Cat 还是 Dog,就可以直接调用 eat 方法,程序会自动判断出来应该如何执行 eat
  • 存取器(getter & setter):用以改变属性的读取和赋值行为
  • 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如 public 表示公有属性或方法
  • 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现
  • 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口

ES6 中类的用法

下面我们先回顾一下 ES6 中类的用法,更详细的介绍可以参考 ECMAScript 6 入门 - Class

属性和方法

使用 class 定义类,使用 constructor 定义构造函数。

通过 new 生成新实例的时候,会自动调用构造函数。

class Animal {
    public name;
    constructor(name) {
        this.name = name;
    }
    sayHi() {
        return `My name is ${this.name}`;
    }
}
​
let a = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack

类的继承

使用 extends 关键字实现继承,子类中使用 super 关键字来调用父类的构造函数和方法。

class Cat extends Animal {
  constructor(name) {
    super(name); // 调用父类的 constructor(name)
    console.log(this.name);
  }
  sayHi() {
    return 'Meow, ' + super.sayHi(); // 调用父类的 sayHi()
  }
}
​
let c = new Cat('Tom'); // Tom
console.log(c.sayHi()); // Meow, My name is Tom

存取器

使用 getter 和 setter 可以改变属性的赋值和读取行为:

class Animal {
  constructor(name) {
    this.name = name;
  }
  get name() {
    return 'Jack';
  }
  set name(value) {
    console.log('setter: ' + value);
  }
}
​
let a = new Animal('Kitty'); // setter: Kitty
a.name = 'Tom'; // setter: Tom
console.log(a.name); // Jack

静态方法

使用 static 修饰符修饰的方法称为静态方法,它们不需要实例化,而是直接通过类来调用:

class Animal {
  static isAnimal(a) {
    return a instanceof Animal;
  }
}
​
let a = new Animal('Jack');
Animal.isAnimal(a); // true
a.isAnimal(a); // TypeError: a.isAnimal is not a function

ES7 中类的用法

ES7 中有一些关于类的提案,TypeScript 也实现了它们,这里做一个简单的介绍。

实例属性

ES6 中实例的属性只能通过构造函数中的 this.xxx 来定义,ES7 提案中可以直接在类里面定义:

class Animal {
  name = 'Jack';
​
  constructor() {
    // ...
  }
}
​
let a = new Animal();
console.log(a.name); // Jack

静态属性

ES7 提案中,可以使用 static 定义一个静态属性:

class Animal {
  static num = 42;
​
  constructor() {
    // ...
  }
}
​
console.log(Animal.num); // 42

TypeScript 中类的用法

public private 和 protected

TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 publicprivateprotected

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public
  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的

下面举一些例子:

class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
}
​
let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';
console.log(a.name); // Tom

上面的例子中,name 被设置为了 public,所以直接访问实例的 name 属性是允许的。

很多时候,我们希望有的属性是无法直接存取的,这时候就可以用 private 了:

class Animal {
  private name;
  public constructor(name) {
    this.name = name;
  }
}
​
let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';
​
// index.ts(9,13): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
// index.ts(10,1): error TS2341: Property 'name' is private and only accessible within class 'Animal'.

需要注意的是,TypeScript 编译之后的代码中,并没有限制 private 属性在外部的可访问性。

上面的例子编译后的代码是:

var Animal = (function () {
  function Animal(name) {
    this.name = name;
  }
  return Animal;
})();
var a = new Animal('Jack');
console.log(a.name);
a.name = 'Tom';

使用 private 修饰的属性或方法,在子类中也是不允许访问的:

class Animal {
  private name;
  public constructor(name) {
    this.name = name;
  }
}
​
class Cat extends Animal {
  constructor(name) {
    super(name);
    console.log(this.name);
  }
}
​
// index.ts(11,17): error TS2341: Property 'name' is private and only accessible within class 'Animal'.

而如果是用 protected 修饰,则允许在子类中访问:

class Animal {
  protected name;
  public constructor(name) {
    this.name = name;
  }
}
​
class Cat extends Animal {
  constructor(name) {
    super(name);
    console.log(this.name);
  }
}

当构造函数修饰为 private 时,该类不允许被继承或者实例化:

class Animal {
  public name;
  private constructor(name) {
    this.name = name;
  }
}
class Cat extends Animal {
  constructor(name) {
    super(name);
  }
}
​
let a = new Animal('Jack');
​
// index.ts(7,19): TS2675: Cannot extend a class 'Animal'. Class constructor is marked as private.
// index.ts(13,9): TS2673: Constructor of class 'Animal' is private and only accessible within the class declaration.

当构造函数修饰为 protected 时,该类只允许被继承:

class Animal {
  public name;
  protected constructor(name) {
    this.name = name;
  }
}
class Cat extends Animal {
  constructor(name) {
    super(name);
  }
}
​
let a = new Animal('Jack');
​
// index.ts(13,9): TS2674: Constructor of class 'Animal' is protected and only accessible within the class declaration.

参数属性

修饰符和readonly还可以使用在构造函数参数中,等同于类中定义该属性同时给该属性赋值,使代码更简洁。

class Animal {
  // public name: string;
  public constructor(public name) {
    // this.name = name;
  }
}

readonly

只读属性关键字,只允许出现在属性声明或索引签名或构造函数中。

class Animal {
  readonly name;
  public constructor(name) {
    this.name = name;
  }
}
​
let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';
​
// index.ts(10,3): TS2540: Cannot assign to 'name' because it is a read-only property.

注意如果 readonly 和其他访问修饰符同时存在的话,需要写在其后面。

class Animal {
  // public readonly name;
  public constructor(public readonly name) {
    // this.name = name;
  }
}

抽象类

abstract 用于定义抽象类和其中的抽象方法。

什么是抽象类?

首先,抽象类是不允许被实例化的:

abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}
​
let a = new Animal('Jack');
​
// index.ts(9,11): error TS2511: Cannot create an instance of the abstract class 'Animal'.

上面的例子中,我们定义了一个抽象类 Animal,并且定义了一个抽象方法 sayHi。在实例化抽象类的时候报错了。

其次,抽象类中的抽象方法必须被子类实现:

abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}
​
class Cat extends Animal {
  public eat() {
    console.log(`${this.name} is eating.`);
  }
}
​
let cat = new Cat('Tom');
​
// index.ts(9,7): error TS2515: Non-abstract class 'Cat' does not implement inherited abstract member 'sayHi' from class 'Animal'.

上面的例子中,我们定义了一个类 Cat 继承了抽象类 Animal,但是没有实现抽象方法 sayHi,所以编译报错了。

下面是一个正确使用抽象类的例子:

abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}
​
class Cat extends Animal {
  public sayHi() {
    console.log(`Meow, My name is ${this.name}`);
  }
}
​
let cat = new Cat('Tom');

上面的例子中,我们实现了抽象方法 sayHi,编译通过了。

需要注意的是,即使是抽象方法,TypeScript 的编译结果中,仍然会存在这个类,上面的代码的编译结果是:

var __extends =
  (this && this.__extends) ||
  function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() {
      this.constructor = d;
    }
    d.prototype = b === null ? Object.create(b) : ((__.prototype = b.prototype), new __());
  };
var Animal = (function () {
  function Animal(name) {
    this.name = name;
  }
  return Animal;
})();
var Cat = (function (_super) {
  __extends(Cat, _super);
  function Cat() {
    _super.apply(this, arguments);
  }
  Cat.prototype.sayHi = function () {
    console.log('Meow, My name is ' + this.name);
  };
  return Cat;
})(Animal);
var cat = new Cat('Tom');

类的类型

给类加上 TypeScript 的类型很简单,与接口类似:

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  sayHi(): string {
    return `My name is ${this.name}`;
  }
}
​
let a: Animal = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack

函数

函数类型

为函数定义类型

// 函数声明
function add(x: number, y: number): number {
    return x + y;
}
// 函数表达式
let myAdd = function(x: number, y: number): number { return x + y; };

注意,输入多余的(或者少于要求的)参数,是不被允许的

书写完整函数类型

现在我们已经为函数指定了类型,下面让我们写出函数的完整类型。

let myAdd: (x: number, y: number) => void = function (
  a: number,
  b: number
): number {
  return a + b;
};
console.log(myAdd(1,2))

注意不要混淆了 TypeScript 中的 => 和 ES6 中的 =>

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

可选参数

前面提到,输入多余的(或者少于要求的)参数,是不允许的。那么如何定义可选的参数呢?

与接口中的可选属性类似,我们用 ? 表示可选的参数:

function buildName(firstName: string, lastName?: string) {
    if (lastName) {
        return firstName + ' ' + lastName;
    } else {
        return firstName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了

function buildName(firstName?: string, lastName: string) {
    if (firstName) {
        return firstName + ' ' + lastName;
    } else {
        return lastName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName(undefined, 'Tom');
​
// index.ts(1,40): error TS1016: A required parameter cannot follow an optional parameter.

参数默认值

在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数

function buildName(firstName: string, lastName: string = 'Cat') {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

此时就不受「可选参数必须接在必需参数后面」的限制了:

function buildName(firstName: string = 'Tom', lastName: string) {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');

剩余参数

ES6 中,可以使用 ...rest 的方式获取函数中的剩余参数(rest 参数):

function push(array, ...items) {
    items.forEach(function(item) {
        array.push(item);
    });
}
​
let a: any[] = [];
push(a, 1, 2, 3);

事实上,items 是一个数组。所以我们可以用数组的类型来定义它:

function push(array: any[], ...items: any[]) {
    items.forEach(function(item) {
        array.push(item);
    });
}
​
let a = [];
push(a, 1, 2, 3);

注意,rest 参数只能是最后一个参数,关于 rest 参数,可以参考 ES6 中的 rest 参数

this

学习如何在JavaScript里正确使用this就好比一场成年礼。 由于TypeScript是JavaScript的超集,TypeScript程序员也需要弄清 this工作机制并且当有bug的时候能够找出错误所在。 幸运的是,TypeScript能通知你错误地使用了 this的地方。 如果你想了解JavaScript里的 this是如何工作的,那么首先阅读Yehuda Katz写的Understanding JavaScript Function Invocation and "this"。 Yehuda的文章详细的阐述了 this的内部工作原理,因此我们这里只做简单介绍。

this和箭头函数

JavaScript里,this的值在函数被调用的时候才会指定。 这是个既强大又灵活的特点,但是你需要花点时间弄清楚函数调用的上下文是什么。 但众所周知,这不是一件很简单的事,尤其是在返回一个函数或将函数当做参数传递的时候。

下面看一个例子:

let deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    createCardPicker: function() {
        return function() {
            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);

可以看到createCardPicker是个函数,并且它又返回了一个函数。 如果我们尝试运行这个程序,会发现它并没有弹出对话框而是报错了。 因为 createCardPicker返回的函数里的this被设置成了window而不是deck对象。 因为我们只是独立的调用了 cardPicker()。 顶级的非方法式调用会将 this视为window。 (注意:在严格模式下, thisundefined而不是window)。

为了解决这个问题,我们可以在函数被返回时就绑好正确的this。 这样的话,无论之后怎么使用它,都会引用绑定的‘deck’对象。 我们需要改变函数表达式来使用ECMAScript 6箭头语法。 箭头函数能保存函数创建时的 this值,而不是调用时的值:

let deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    createCardPicker: function() {
        // NOTE: the line below is now an arrow function, allowing us to capture 'this' right here
        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);

更好事情是,TypeScript会警告你犯了一个错误,如果你给编译器设置了--noImplicitThis标记。 它会指出 this.suits[pickedSuit]里的this的类型为any

this参数

不幸的是,this.suits[pickedSuit]的类型依旧为any。 这是因为 this来自对象字面量里的函数表达式。 修改的方法是,提供一个显式的 this参数。 this参数是个假的参数,它出现在参数列表的最前面:

function f(this: void) {
    // make sure `this` is unusable in this standalone function
}

让我们往例子里添加一些接口,CardDeck,让类型重用能够变得清晰简单些:

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

现在TypeScript知道createCardPicker期望在某个Deck对象上调用。 也就是说 thisDeck类型的,而非any,因此--noImplicitThis不会报错了。

this参数在回调函数里

你可以也看到过在回调函数里的this报错,当你将一个函数传递到某个库函数里稍后会被调用时。 因为当回调被调用的时候,它们会被当成一个普通函数调用, this将为undefined。 稍做改动,你就可以通过 this参数来避免错误。 首先,库函数的作者要指定 this的类型:

interface UIElement {
    addClickListener(onclick: (this: void, e: Event) => void): void;
}

this: void means that addClickListener expects onclick to be a function that does not require a this type. Second, annotate your calling code with this:

class Handler {
    info: string;
    onClickBad(this: Handler, e: Event) {
        // oops, used this here. using this callback would crash at runtime
        this.info = e.message;
    }
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // error!

指定了this类型后,你显式声明onClickBad必须在Handler的实例上调用。 然后TypeScript会检测到 addClickListener要求函数带有this: void。 改变 this类型来修复这个错误:

class Handler {
    info: string;
    onClickGood(this: void, e: Event) {
        // can't use this here because it's of type void!
        console.log('clicked!');
    }
}
let h = new Handler();
uiElement.addClickListener(h.onClickGood);

因为onClickGood指定了this类型为void,因此传递addClickListener是合法的。 当然了,这也意味着不能使用 this.info. 如果你两者都想要,你不得不使用箭头函数了:

class Handler {
    info: string;
    onClickGood = (e: Event) => { this.info = e.message }
}

这是可行的因为箭头函数不会捕获this,所以你总是可以把它们传给期望this: void的函数。 缺点是每个 Handler对象都会创建一个箭头函数。 另一方面,方法只会被创建一次,添加到 Handler的原型链上。 它们在不同 Handler对象间是共享的。

重载

重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。

比如,我们需要实现一个函数 reverse,输入数字 123 的时候,输出反转的数字 321,输入字符串 'hello' 的时候,输出反转的字符串 'olleh'

利用联合类型,我们可以这么实现:

function reverse(x: number | string): number | string {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

然而这样有一个缺点,就是不能够精确的表达,输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串。

这时,我们可以使用重载定义多个 reverse 的函数类型:

function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

泛型

介绍

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

当我们需要写一个传入什么类型就得到什么类型的函数

  • function one(a: any) : any{
      return a;
    }
    
  • function one(a: any) : any{
        if(typeof a === 'number') {
            let ret = (a as number)
            return ret ;
        }
        return a;
    }
    //每一种类型都写一个方法
    
  • function one<T>(a: T) : T{
        return a;
    }
    let a1 = one<number>(1)
    let a2 = one(520)
    //描述T是什么类型的时候,你可以在<number>描述它是一个 number类型,同样也可以这样描述描述(a: T) 对应(520), T 就是 520的类型。
    

泛型函数

不用泛型的话,这个函数可能是下面这样:

function identity(arg: number): number {
    return arg;
}

或者,我们使用any类型来定义函数:

function identity(arg: any): any {
    return arg;
}

使用any类型会导致这个函数可以接收任何类型的arg参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。

因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 类型变量,它是一种特殊的变量,只用于表示类型而不是值。

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

generic-type-filled.jpg

我们给identity添加了类型变量TT帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。 之后我们再次使用了 T当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。

我们定义了泛型函数后,可以用两种方法使用。

  • 第一种是,传入所有的参数,包含类型参数:

    let output = identity<string>("myString");  // type of output will be 'string'
    

    这里我们明确的指定了Tstring类型,并做为一个参数传给函数,使用了<>括起来而不是()

  • 第二种方法更普遍。利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定T的类型:

    let output = identity("myString");  // type of output will be 'string'
    

    注意我们没必要使用尖括号(<>)来明确地传入类型;编译器可以查看myString的值,然后把T设置为它的类型。

使用泛型变量

使用泛型创建像identity这样的泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型。 换句话说,你必须把这些参数当做是任意或所有类型。

看下之前identity例子:

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

如果我们想同时打印出arg的长度。我们很可能会这样做:

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

如果这么做,编译器会报错说我们使用了arg.length属性,但是没有地方指明arg具有这个属性。 记住,这些类型变量代表的是任意类型,所以使用这个函数的人可能传入的是个数字,而数字是没有 .length属性的。

现在假设我们想操作T类型的数组而不直接是T。由于我们操作的是数组,所以.length属性是应该存在的。 我们可以像创建其它数组一样创建这个数组:

function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}

你可以这样理解loggingIdentity的类型:泛型函数loggingIdentity,接收类型参数T和参数arg,它是个元素类型是T的数组,并返回元素类型是T的数组。 如果我们传入数字数组,将返回一个数字数组,因为此时 T的的类型为number。 这可以让我们把泛型变量T当做类型的一部分使用,而不是整个类型,增加了灵活性。

我们也可以这样实现上面的例子:

function loggingIdentity<T>(arg: Array<T>): Array<T> {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}

泛型类

泛型类看上去与泛型接口差不多。 泛型类使用( <>)括起泛型类型,跟在类名后面。

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}
​
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
console.log(myGenericNumber.add(1,2))

类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。

泛型接口

interface GenericIdentityFn {
    <T>(arg: T): T;
}
function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: GenericIdentityFn = identity;
​

我们可以把泛型参数提前到接口名上:

interface Person<T> {
    name: T;
    getAge(arg: T): T;
}
​
let myIdentity: Person<string> = {
    name: "兔兔",
    getAge(name) {
        return name
    }
};。

泛型约束extends

你应该会记得之前的一个例子,我们有时候想操作某类型的一组值,并且我们知道这组值具有什么样的属性。 在 loggingIdentity例子中,我们想访问arglength属性,但是编译器并不能证明每种类型都有length属性,所以就报错了。

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

相比于操作any所有类型,我们想要限制函数去处理任意带有.length属性的所有类型。 只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于T的约束要求。

为此,我们定义一个接口来描述约束条件。 创建一个包含 .length属性的接口,使用这个接口和extends关键字来实现约束:

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

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

loggingIdentity(3);  // Error, 类型“number”的参数不能赋给类型“Lengthwise”的参数。

我们需要传入符合约束类型的值,必须包含必须的属性:

loggingIdentity({length: 10, value: 3});

高级类型

交叉类型

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如,type是type1和type2接口的交集。 就是说这个类型的对象同时拥有了这二种类型的成员。

interface type1{
    a1:number,
    a2:string
}
interface type2{
    b1:number,
    b2:string
}
interface type<t,k>{
   a:t,
   b:k
}
let c:type<type1,type2> = {
   a:{
       a1:1,
       a2:'1'
   },
   b:{
       b1:1,
       b2:'1'
   }
}
console.log(c)//{ a: { a1: 1, a2: '1' }, b: { b1: 1, b2: '1' } } 
interface type1{
    a1:number,
    a2:string
}
interface type2{
    b1:number,
    b2:string
}
interface type<t,k>{
   a1,
   a2,
   b1,
   b2
}
let c:type<type1,type2> = {
       a1:1,
       a2:'1',
       b1:1,
       b2:'1'
}
console.log(c)//{ a1: 1, a2: '1', b1: 1, b2: '1' } 

联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种。

简单的例子

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
let myFavoriteNumber: string | number;
myFavoriteNumber = true;
​
// index.ts(2,1): error TS2322: Type 'boolean' is not assignable to type 'string | number'.
//   Type 'boolean' is not assignable to type 'number'.

联合类型使用 | 分隔每个类型。

这里的 let myFavoriteNumber: string | number 的含义是,允许 myFavoriteNumber 的类型是 string 或者 number,但是不能是其他类型。

访问联合类型的属性或方法

当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法

function getLength(something: string | number): number {
    return something.length;
}
​
// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
//   Property 'length' does not exist on type 'number'.

上例中,length 不是 stringnumber 的共有属性,所以会报错。

访问 stringnumber 的共有属性是没问题的:

function getString(something: string | number): string {
    return something.toString();
}

联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型:

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // 编译时报错
​
// index.ts(5,30): error TS2339: Property 'length' does not exist on type 'number'.

上例中,第二行的 myFavoriteNumber 被推断成了 string,访问它的 length 属性不会报错。

而第四行的 myFavoriteNumber 被推断成了 number,访问它的 length 属性时就报错了。

typeof类型保护

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. 构造签名所返回的类型的联合

以此顺序。

类型别名type

type关键字

说明:字面意思,用来给一个类型起个新名字。生成一个接口

type str1 = string;
type str2 = ()=>string;   //此为函数类型形状,注意跟下面区分
type str = str1 | str2;
​
let s:str = "hello";
let s1:str = () =>"heihei";   //为箭头函数

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

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

type 与 interface 的区别

接口VS类型别名 相同点: 都可以描述一个对象或者函数 都允许拓展(extends和交叉类型) 不同点: type 可以声明基本类型别名,联合类型,元组等类型 type 语句中还可以使用 typeof 获取实例的 类型进行赋值 interface 能够声明合并

相似的

type 用于定义数据的类型别名。interface 用于定义数据的类型别名。

type User = {
    name1: string
    age: number
  };
let user:User={
    name1:'1',
    age:1
}

interface User {
    name1: string
    age: number
  };
let user:User={
    name1:'1',
    age:1
}
不相似的

typeinterface 都可以实现继承,但是他们的表现形式不同。

//interface extends interface

interface Name {
  name: string;
}
interface User extends Name {
  age: number;
}

//type 与 type 交叉

type Name = {
  name: string;
}
type User = Name & { age: number  };

//interface extends type

type Name = {
  name: string;
}
interface User extends Name {
  age: number;
}

//type 与 interface 交叉

interface Name {
  name: string;
}
type User = Name & {
  age: number;
}

因为 type 作为类型的别名,因此可以轻易的实现声明基本类型别名,联合类型,元组等类型,而 interface 则不行。

// 基本类型别名
type Name = string// 联合类型,
interface Dog {
    a:number
}
interface Cat {
    b:string
}
type Pet = Dog | Cat
let c:Pet={a:1}
​
// 具体定义数组每个位置的类型
type PetList = [Dog, Pet]

interface 能够声明合并,而 type 不行(会报重复声明错误)。

interface User {
    name: string,
    age: number,
}
interface User {
    sex: string,
}
let a:User={
    name:'q',
    age:1,
    sex:'men'
}
type guest = {
    name: string,
    age: number,
}
type guest = {
    sex: string,
}
//type 报错

\