TypeScript

什么是TypeScript

TypeScript是由微软公司开发的一个开源JavaScript的超集,主要提供了类型系统和对ES6的支持,他可以编译成纯
JavaScript. 任何现有的 JavaScript 都是合法的 TypeScript 程序。

TypeScript从出现至今已经成为了前端领域中不可忽视的技术,各大流行框架都已经支持使用 TypeScript 作为开发语言。

安装 TypeScript

TypeScript 最终要运行起来,我们需要将 TypeScript 代码转换成对应的 JavaScript 代码,那么 TypeScript 的命令行工具就可以帮我们完成这件事情。

TypeScript 的命令行工具安装方法如下:

npm install -g typescript

以上命令会在全局环境下安装 tsc 命令,安装完成之后,我们就可以在任何地方执行 tsc 命令了。

编译一个 TypeScript 文件很简单:

tsc hello.ts

我们约定使用 TypeScript 编写的文件以 .ts 为后缀

TypeScript初体验

接下来我们书写一段代码,大家不需要纠结详细的技术点,只需要看 TypeScript 给我们带来的功能体验即可。

function greeter(msg: string) {
  return "Hello, " + msg;
}

let str = "World";
console.log(greeter(str));

上面这段代码就是一段 TypeScript代码,我们在这段代码中规定了函数greeter传入的参数必须类型是string。我们将这段代码保存为greeter.ts文件

接下来我们在命令行中执行

tsc greeter.ts

这个命令会将我们写好的 ts 文件转换成相应的 JavaScript 代码文件

function greeter(msg) {
  return "Hello, " + msg;
}
var str = "World";
console.log(greeter(msg));

上述例子中,我们用 : 指定 person 参数类型为 string。但是编译为 js 之后,并没有什么检查的代码被插入进来。

TypeScript 只会进行静态检查,如果发现有错误,编译的时候就会报错

下面尝试把这段代码编译一下:

function greeter(msg: string) {
  return "Hello, " + msg;
}

let str = [0, 1, 2];
console.log(greeter(str));

编辑器中会提示错误,编译的时候也会出错:

index.ts(6,22): error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.

但是还是生成了 js 文件:

function greeter(msg) {
  return "Hello, " + msg;
}

let str = [0, 1, 2];
console.log(greeter(str));

TypeScript 编译的时候即使报错了,还是会生成编译结果,我们仍然可以使用这个编译之后的文件。

TypeScript数据类型

介绍

为了让程序有价值,我们需要能够处理最简单的数据单元:数字,字符串,结构体,布尔值等。
TypeScript 支持与 JavaScript 几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。

布尔值

最基本的数据类型就是简单的 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;

字符串

JavaScript 程序的另一项基本操作是处理网页或服务器端的文本数据。
像其它语言里一样,我们使用string表示文本数据类型。
和 JavaScript 一样,可以使用双引号(")或单引号(')表示字符串。

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

数组

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)类型

联合类型是高级主题,我们会在以后的章节里讨论它。

枚举

enum类型是对 JavaScript 标准数据类型的一个补充。
像 C###等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。

enum Color {
  Red,
  Green,
  Blue
}
let c: Color = Color.Green;

默认情况下,从0开始为元素编号。
你也可以手动的指定成员的数值。
例如,我们将上面的例子改成从1开始编号:

enum Color {
  Red = 1,
  Green,
  Blue
}
let c: Color = Color.Green;

或者,全部都采用手动赋值:

enum Color {
  Red = 1,
  Green = 2,
  Blue = 4
}
let c: Color = Color.Green;

枚举类型提供的一个便利是你可以由枚举的值得到它的名字。
例如,我们知道数值为 2,但是不确定它映射到 Color 里的哪个名字,我们可以查找相应的名字:

enum Color {
  Red = 1,
  Green,
  Blue
}
let colorName: string = Color[2];

alert(colorName); // 显示'Green'因为上面代码里它的值是2

任意值

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

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

在对现有代码进行改写的时候,any类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。
你可能认为Object有相似的作用,就像它在其它语言中那样。
但是Object类型的变量只是允许你给它赋任意值 - 但是却不能够在它上面调用任意的方法,即便它真的有这些方法:

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;

空值

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

function warnUser(): void {
  alert("This is my warning message");
}

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

let unusable: void = undefined;

Null 和
Undefined

TypeScript
里,undefinednull两者各自有自己的类型分别叫做undefinednull
void相似,它们的本身的类型用处不是很大:

// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;

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

然而,当你指定了--strictNullChecks标记,nullundefined只能赋值给void和它们各自。
这能避免很多常见的问题。
也许在某处你想传入一个stringnullundefined,你可以使用联合类型string | null | undefined
再次说明,稍后我们会介绍联合类型。

注意:我们鼓励尽可能地使用--strictNullChecks,但在本手册里我们假设这个标记是关闭的。

Never

never类型表示的是那些永不存在的值的类型。
例如,never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型;
变量也可能是never类型,当它们被永不为真的类型保护所约束时。

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

下面是一些返回never类型的函数:

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

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

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

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

类型断言

有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。
通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。
类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。
它没有运行时的影响,只是在编译阶段起作用。
TypeScript 会假设你,程序员,已经进行了必须的检查。

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

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;

两种形式是等价的。
至于使用哪个大多数情况下是凭个人喜好;然而,当你在 TypeScript 里使用 JSX 时,只有as语法断言是被允许的。

关于let

你可能已经注意到了,我们使用let关键字来代替大家所熟悉的 JavaScript 关键字var
let关键字是 JavaScript 的一个新概念,TypeScript 实现了它。
我们会在以后详细介绍它,很多常见的问题都可以通过使用let来解决,所以尽可能地使用let来代替var吧。

介绍

传统的 JavaScript 程序使用函数和基于原型的继承来创建可重用的组件,但对于熟悉使用面向对象方式的程序员来讲就有些棘手,因为他们用的是基于类的继承并且对象是由类构建出来的。
从 ECMAScript 2015,也就是 ECMAScript 6 开始,JavaScript 程序员将能够使用基于类的面向对象的方式。
使用 TypeScript,我们允许开发者现在就使用这些特性,并且编译后的 JavaScript 可以在所有主流浏览器和平台上运行,而不需要等到下个 JavaScript 版本。

下面看一个使用类的例子:

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}

let greeter = new Greeter("world");

如果你使用过 C#或 Java,你会对这种语法非常熟悉。
我们声明一个Greeter类。这个类有 3 个成员:一个叫做greeting的属性,一个构造函数和一个greet方法。

你会注意到,我们在引用任何一个类成员的时候都用了this
它表示我们访问的是类的成员。

最后一行,我们使用new构造了Greeter类的一个实例。
它会调用之前定义的构造函数,创建一个Greeter类型的新对象,并执行构造函数初始化它。

继承

在 TypeScript 里,我们可以使用常用的面向对象模式。
基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类。

看下面的例子:

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

class Dog extends Animal {
  bark() {
    console.log("Woof! Woof!");
  }
}

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

这个例子展示了最基本的继承:类从基类中继承了属性和方法。
这里,Dog是一个派生类,它派生自Animal基类,通过extends关键字。
派生类通常被称作子类,基类通常被称作超类

因为Dog继承了Animal的功能,因此我们可以创建一个Dog的实例,它能够bark()move()

下面我们来看个更加复杂的例子。

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

这个例子展示了一些上面没有提到的特性。
这一次,我们使用extends关键字创建了Animal的两个子类:HorseSnake

与前一个例子的不同点是,派生类包含了一个构造函数,它必须调用super(),它会执行基类的构造函数。
而且,在构造函数里访问this的属性之前,我们一定要调用super()
这个是 TypeScript 强制执行的一条重要规则。

这个例子演示了如何在子类里可以重写父类的方法。
Snake类和Horse类都创建了move方法,它们重写了从Animal继承来的move方法,使得move方法根据不同的类而具有不同的功能。
注意,即使tom被声明为Animal类型,但因为它的值是Horse,调用tom.move(34)时,它会调用Horse里重写的方法:

Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.

公共,私有与受保护的修饰符

默认为public

在上面的例子里,我们可以自由的访问程序里定义的成员。
如果你对其它语言中的类比较了解,就会注意到我们在之前的代码里并没有使用public来做修饰;例如,C#要求必须明确地使用public指定成员是可见的。
在 TypeScript 里,成员都默认为public

你也可以明确的将一个成员标记成public
我们可以用下面的方式来重写上面的Animal类:

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

理解private

当成员被标记成private时,它就不能在声明它的类的外部访问。比如:

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

new Animal("Cat").name; // 错误: 'name' 是私有的.

TypeScript 使用的是结构性类型系统。
当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。

然而,当我们比较带有privateprotected成员的类型的时候,情况就不同了。
如果其中一个类型里包含一个private成员,那么只有当另外一个类型中也存在这样一个private成员,
并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。
对于protected成员也使用这个规则。

下面来看一个例子,更好地说明了这一点:

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 不兼容.

这个例子中有AnimalRhino两个类,RhinoAnimal类的子类。
还有一个Employee类,其类型看上去与Animal是相同的。
我们创建了几个这些类的实例,并相互赋值来看看会发生什么。
因为AnimalRhino共享了来自Animal里的私有成员定义private name: string,因此它们是兼容的。
然而Employee却不是这样。当把Employee赋值给Animal的时候,得到一个错误,说它们的类型不兼容。
尽管Employee里也有一个私有成员name,但它明显不是Animal里面定义的那个。

理解protected

protected修饰符与private修饰符的行为很相似,但有一点不同,protected成员在派生类中仍然可以访问。例如:

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

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");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 错误

注意,我们不能在Person类外使用name,但是我们仍然可以通过Employee类的实例方法访问,因为Employee是由Person派生而来的。

构造函数也可以被标记成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' 的构造函数是被保护的.

readonly 修饰符

你可以使用readonly关键字将属性设置为只读的。
只读属性必须在声明时或构造函数里被初始化。

class Octopus {
  readonly name: string;
  readonly numberOfLegs: number = 8;
  constructor(theName: string) {
    this.name = theName;
  }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.

参数属性

在上面的例子中,我们不得不定义一个受保护的成员name和一个构造函数参数theNamePerson类里,并且立刻将theName的值赋给name
这种情况经常会遇到。参数属性可以方便地让我们在一个地方定义并初始化一个成员。
下面的例子是对之前Animal类的修改版,使用了参数属性:

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

注意看我们是如何舍弃了theName,仅在构造函数里使用private name: string参数来创建和初始化name成员。
我们把声明和赋值合并至一处。

参数属性通过给构造函数参数添加一个访问限定符来声明。
使用private限定一个参数属性会声明并初始化一个私有成员;对于publicprotected来说也是一样。

存取器

TypeScript 支持通过 getters/setters 来截取对对象成员的访问。
它能帮助你有效的控制对对象成员的访问。

下面来看如何把一个简单的类改写成使用getset
首先,我们从一个没有使用存取器的例子开始。

class Employee {
  fullName: string;
}

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

我们可以随意的设置fullName,这是非常方便的,但是这也可能会带来麻烦。

下面这个版本里,我们先检查用户密码是否正确,然后再允许其修改员工信息。
我们把对fullName的直接访问改成了可以检查密码的set方法。
我们也加了一个get方法,让上面的例子仍然可以工作。

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

我们可以修改一下密码,来验证一下存取器是否是工作的。当密码不对时,会提示我们没有权限去修改员工。

对于存取器有下面几点需要注意的:

首先,存取器要求你将编译器设置为输出 ECMAScript 5 或更高。
不支持降级到 ECMAScript 3。
其次,只带有get不带有set的存取器自动被推断为readonly

接口

介绍

TypeScript 的核心原则之一是对值所具有的结构进行类型检查。
它有时被称做“鸭式辨型法”或“结构性子类型化”。
在 TypeScript 里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

接口初探

下面通过一个简单示例来观察接口是如何工作的:

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

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

类型检查器会查看printLabel的调用。
printLabel有一个参数,并要求这个对象参数有一个名为label类型为string的属性。
需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。
然而,有些时候 TypeScript 却并不会这么宽松,我们下面会稍做讲解。

下面我们重写上面的例子,这次使用接口来描述:必须包含一个label属性且类型为string

interface LabelledValue {
  label: string;
}

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

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

LabelledValue接口就好比一个名字,用来描述上面例子里的要求。
它代表了有一个label属性且类型为string的对象。
需要注意的是,我们在这里并不能像在其它语言里一样,说传给printLabel的对象实现了这个接口。我们只会去关注值的外形。
只要传入的对象满足上面提到的必要条件,那么它就是被允许的。

还有一点值得提的是,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。

可选属性

接口里的属性不全都是必需的。
有些是只在某些条件下存在,或者根本不存在。
可选属性在应用“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" });

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

可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。
比如,我们故意将createSquare里的color属性名拼错,就会得到一个错误提示:

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

function createSquare(config: SquareConfig): { color: string; area: number } {
  let newSquare = { color: "white", area: 100 };
  if (config.clor) {
    // Error: Property 'clor' does not exist on type 'SquareConfig'
    newSquare.color = config.clor;
  }
  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
让我们传入{ size: number; label: string; }到仅期望得到{ label: string; }的函数里。
我们已经学过了可选属性,并且知道他们在“option bags”模式里很有用。

然而,天真地将这两者结合的话就会像在 JavaScript 里那样搬起石头砸自己的脚。
比如,拿createSquare例子来说:

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

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

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

注意传入createSquare的参数拼写为colour而不是color
在 JavaScript 里,这会默默地失败。

你可能会争辩这个程序已经正确地类型化了,因为width属性是兼容的,不存在color属性,而且额外的colour属性是无意义的。

然而,TypeScript 会认为这段代码可能存在 bug。
对象字面量会被特殊对待而且会经过额外属性检查,当将它们赋值给变量或作为参数传递的时候。
如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。

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

绕开这些检查非常简单。
最简便的方法是使用类型断言:

let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

然而,最佳的方式是能够添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。
如果SquareConfig带有上面定义的类型的colorwidth属性,并且还会带有任意数量的其它属性,那么我们可以这样定义它:

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

我们稍后会讲到索引签名,但在这我们要表示的是SquareConfig可以有任意数量的属性,并且只要它们不是colorwidth,那么就无所谓它们的类型是什么。

还有最后一种跳过这些检查的方式,这可能会让你感到惊讶,它就是将这个对象赋值给一个另一个变量:
因为squareOptions不会经过额外属性检查,所以编译器不会报错。

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

要留意,在像上面一样的简单代码里,你可能不应该去绕开这些检查。
对于包含方法和内部状态的复杂对象字面量来讲,你可能需要使用这些技巧,但是大部额外属性检查错误是真正的 bug。
就是说你遇到了额外类型检查出的错误,比如“option bags”,你应该去审查一下你的类型声明。
在这里,如果支持传入colorcolour属性到createSquare,你应该修改SquareConfig定义来体现出这一点。

函数类型

接口能够描述 JavaScript 中对象拥有的各种各样的外形。
除了描述带有属性的普通对象外,接口也可以描述函数类型。

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

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

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

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

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

let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
  let result = src.search(sub);
  return result > -1;
};

函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。
如果你不想指定类型,TypeScript 的类型系统会推断出参数类型,因为函数直接赋值给了SearchFunc类型变量。
函数的返回值类型是通过其返回值推断出来的(此例是falsetrue)。
如果让这个函数返回数字或字符串,类型检查器会警告我们函数的返回值类型与SearchFunc接口中的定义不匹配。

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

类类型

实现接口

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

interface ClockInterface {
  currentTime: Date;
}

class Clock implements ClockInterface {
  currentTime: Date;
  constructor(h: number, m: number) {}
}

你也可以在接口中描述一个方法,在类里实现它,如同下面的setTime方法一样:

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

class Clock implements ClockInterface {
  currentTime: Date;
  setTime(d: Date) {
    this.currentTime = d;
  }
  constructor(h: number, m: number) {}
}

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

继承接口

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

interface Shape {
  color: string;
}

interface Square extends Shape {
  sideLength: number;
}

let square = <Square>{};
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;

接口继承类

当接口继承了一个类类型时,它会继承类的成员但不包括其实现。
就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。
接口同样会继承到类的 private 和 protected 成员。
这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。

当你有一个庞大的继承结构时这很有用,但要指出的是你的代码只在子类拥有特定属性时起作用。
除了继承自基类,子类之间不必相关联。
例:

class Control {
  private state: any;
}

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

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

class TextBox extends Control {
  select() {}
}

// Error: Property 'state' is missing in type 'Image'.
class Image implements SelectableControl {
  select() {}
}

class Location {}

在上面的例子里,SelectableControl包含了Control的所有成员,包括私有成员state
因为state是私有成员,所以只能够是Control的子类们才能实现SelectableControl接口。
因为只有Control的子类才能够拥有一个声明于Control的私有成员state,这对私有成员的兼容性是必需的。

Control类内部,是允许通过SelectableControl的实例来访问私有成员state的。
实际上,SelectableControl就像Control一样,并拥有一个select方法。
ButtonTextBox类是SelectableControl的子类(因为它们都继承自Control并有select方法),但ImageLocation类并不是这样的。

版权声明:小马大哥哥 发表于 2019-10-20 13:30:13。
转载请注明:TypeScript | 马哥导航

暂无评论

暂无评论...