对象的类型——Interface

简介

在 TypeScript 中,使用接口(Interface)来定义对象的类型(对「对象的形状(Shape)」进行描述)。

在面向对象语言中,接口(Interface)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(class)去实现(implement)。

  1. interface Girl {
  2. readonly id: number; // 只读属性
  3. name: string,
  4. age: number,
  5. bust?: number,// 可选属性
  6. [propName: string]: any, // 定义一个任意属性:key是string,值是any
  7. say(): string // 该方法必须有返回值,且值为string
  8. }

注意:一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集

  1. interface Person {
  2. name: string;
  3. age?: number; // name报错:类型“number | undefined”的属性“age”不能赋给“string”索引类型“string”。
  4. job: undefined; // job报错:类型“undefined”的属性“job”不能赋给“string”索引类型“string”。
  5. [propName: string]: string;
  6. }
  7. let tom: Person = {
  8. name: 'Tom',
  9. age: 25,
  10. job: undefined,
  11. gender: 'male'
  12. };

解决:一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:

  1. interface Person {
  2. name: string;
  3. age?: number;
  4. [propName: string]: string | number | undefined;
  5. }
  6. let tom: Person = {
  7. name: 'Tom',
  8. age: 25,
  9. gender: 'male'
  10. };

接口可以同名,同名的接口会进行合并(导入的接口不行)。type别名不行

  1. interface IUser {
  2. name: string;
  3. }
  4. interface IUser {
  5. age: string;
  6. }
  7. const user: IUser = {
  8. name: 'John',
  9. age: '20',
  10. };

class实现interface

一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements 关键字来实现。一个类可以实现多个接口

  1. interface human {
  2. eat(): void // 描述实例上的方法,或者原型上的方法
  3. }
  4. class Zbb implements Girl,human {
  5. constructor(id: number, name: string, age: number) {
  6. this.id = id;
  7. this.name = name;
  8. this.age = age;
  9. }
  10. id: number;
  11. name: string;
  12. age: number;
  13. say() {
  14. return "1234";
  15. }
  16. eat(): string { // ok,虽然接口描述的返回值是void,可以理解成是接口不关心返回值
  17. return '吃饭'
  18. }
  19. }

interface继承interface

  1. interface Teacher extends Girl {
  2. teach(): string
  3. }

interface继承class

常见的面向对象语言中,接口是不能继承类的,但是在 TypeScript 中却是可以的(接口继承类,只会继承它的实例属性和实例方法。):

  1. class Point {
  2. /** 静态属性,坐标系原点 */
  3. static origin = new Point(0, 0);
  4. /** 静态方法,计算与原点距离 */
  5. static distanceToOrigin(p: Point) {
  6. return Math.sqrt(p.x * p.x + p.y * p.y);
  7. }
  8. /** 实例属性,x 轴的值 */
  9. x: number;
  10. /** 实例属性,y 轴的值 */
  11. y: number;
  12. /** 构造函数 */
  13. constructor(x: number, y: number) {
  14. this.x = x;
  15. this.y = y;
  16. }
  17. /** 实例方法,打印此点 */
  18. printPoint() {
  19. console.log(this.x, this.y);
  20. }
  21. }
  22. // interface PointInstanceType {
  23. // x: number;
  24. // y: number;
  25. // printPoint(): void;
  26. // }
  27. // 等价于 interface Point3d extends PointInstanceType {
  28. interface Point3d extends Point {
  29. z: number;
  30. }
  31. let point3d: Point3d = {
  32. x: 1, y: 2, z: 3,
  33. printPoint: function (): void {
  34. throw new Error('Function not implemented.');
  35. }
  36. };

解析:实际上,当声明 class Point 时,除了会创建一个名为 Point 的类之外,同时也创建了一个名为 Point 的类型(实例的类型)。所以既可以将 Point 当做一个类来用(使用 new Point 创建它的实例),也可以将 Point 当做一个类型来用(使用 : Point 表示参数的类型)。
所以「接口继承类」和「接口继承接口」没有什么本质的区别。值得注意的是,PointInstanceType 相比于 类Point,不包含constructor 方法、静态属性或静态方法(实例的类型当然不应该包括构造函数、静态属性或静态方法)。

数组的类型

在 TypeScript 中,数组类型定义主要有两种方式:「类型 + 方括号」表示法 数组泛型

[类型 + 方括号]表示法

  1. let fibonacci: number[] = [1, 1, 2, 3, 5];

数组泛型

  1. let fibonacci: Array<number> = [1, 1, 2, 3, 5];

用接口表示数组

虽然接口也可以用来描述数组,但是一般不会这么做,因为这种方式比前两种方式复杂多了:

  1. interface NumberArray {
  2. [index: number]: number;
  3. }
  4. let fibonacci: NumberArray = [1, 1, 2, 3, 5];

接口表示数组常用来表示类数组,常用的类数组TypeScript中的内置对象都有自己的定义,如 IArguments, NodeList, HTMLCollection等:

  1. interface IArguments {
  2. [index: number]: any;
  3. length: number;
  4. callee: Function;
  5. }

元组

数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象,超出元组固定长度的越界元素,它的类型会被限制为元组中每个类型的联合类型(主要针对push方法,修改索引下标不行)。

  1. let tuple: [string, number] = ['a', 1] // 初始化的时候必须提供所有元组类型中指定的项。
  2. tuple[0] = 'Tom'; // ok
  3. tuple[1] = 25; // ok
  4. tuple[2] = 25; // error,不能将类型“25”分配给类型“undefined”
  5. tuple[2] = undefined; // error,长度为"2"的元组类型"[string, number]"在索引"2"处没有元素
  6. tuple[0].slice(1); // ok
  7. tuple[1].toFixed(2); // ok
  8. tuple.push(1); // ok ⭐只要push进去的值类型属于元组中每个类型的联合类型即可
  9. tuple.push(true); // error,类型“boolean”的参数不能赋给类型“string | number”的参数

内置对象

JavaScript 中有很多内置对象,可以直接在 TypeScript 中当做定义好了的类型使用。而他们的定义文件,则在 TypeScript 核心库的定义文件中。

ECMAScript 的内置对象

ECMAScript 标准提供的内置对象有:BooleanErrorDateRegExp

  1. let b: Boolean = new Boolean(1);
  2. let e: Error = new Error('Error occurred');
  3. let d: Date = new Date();
  4. let r: RegExp = /[a-z]/;

DOM 和 BOM 的内置对象

DOM 和 BOM 提供的内置对象有:DocumentHTMLElementEventNodeList

  1. let body: HTMLElement = document.body;
  2. let allDiv: NodeList = document.querySelectorAll('div');
  3. document.addEventListener('click', function(e: MouseEvent) {
  4. // Do something
  5. });

TypeScript 核心库的定义文件

TypeScript 核心库的定义文件中定义了所有浏览器环境需要用到的类型,并且是预置在 TypeScript 中的。
在使用一些常用的方法的时候,TypeScript 实际上已经做了很多类型判断的工作了,比如:

  1. Math.pow(10, '2');
  2. // 报错:类型“string”的参数不能赋给类型“number”的参数。

解析:事实上 Math.pow 的类型定义如下:必须接受两个 number 类型的参数

  1. interface Math {
  2. /**
  3. * Returns the value of a base expression taken to a specified power.
  4. * @param x The base value of the expression.
  5. * @param y The exponent value of the expression.
  6. */
  7. pow(x: number, y: number): number;
  8. }

但是Node.js 不是内置对象的一部分,如果想用 TypeScript 写 Node.js,则需要引入第三方声明文件:**npm install @types/node --save-dev**

函数的类型

在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression)。

函数声明

  1. function sum(x: number, y: number): number {
  2. return x + y;
  3. }

函数表达式

  1. let mySum = (x: number, y: number): number => {
  2. return x + y;
  3. };
  4. // 等号右边定义了,左边通过类型推论就可以简化

上面的代码也可以通过编译,但是它只对等号右侧的匿名函数进行了类型定义,而等号左边的 mySum,是通过赋值操作进行类型推论而推断出来的。如果需要手动给 mySum 添加类型,应该是这样:

  1. // 等号两边同时定义
  2. let mySum: (x: number, y: number) => number = (x: number, y: number): number => {
  3. return x + y;
  4. };
  5. // 左边定义了右边就可以简化
  6. let mySum: (x: number, y: number) => number = (x, y) => {
  7. return x + y;
  8. };

注意不要混淆了 TypeScript 中的 => 和 ES6 中的 =>
TypeScript中的**=>** 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。

type声明函数类型

在没有提供函数实现的情况下,type有两种声明函数类型的方式:

  1. type LongHand = {
  2. (a: number): number;
  3. };
  4. type ShortHand = (a: number) => number;

但是想使用函数重载时,只能用第一种方式:

  1. type LongHandAllowsOverloadDeclarations = {
  2. (a: number): number;
  3. (a: string): string;
  4. };

interface声明函数类型

  1. // 表示一个返回值为 string 的函数
  2. interface ReturnString {
  3. (): string;
  4. }
  5. // 可以根据实际来传递任何参数、可选参数以及 rest 参数
  6. interface Complex {
  7. (foo: string, bar?: number, ...others: boolean[]): number;
  8. }
  9. // 函数重载
  10. interface Overloaded {
  11. (foo: string): string;
  12. (foo: number): number;
  13. }
  14. // 实现接口的一个例子:
  15. function stringOrNumber(foo: number): number;
  16. function stringOrNumber(foo: string): string;
  17. function stringOrNumber(foo: any): any {
  18. if (typeof foo === 'number') {
  19. return foo * foo;
  20. } else if (typeof foo === 'string') {
  21. return `hello ${foo}`;
  22. }
  23. }
  24. const overloaded: Overloaded = stringOrNumber;
  25. // 使用
  26. const str = overloaded(''); // str 被推断为 'string'
  27. const num = overloaded(123); // num 被推断为 'number'

可实例化:使用 new 作为前缀,意味着需要使用 new 关键字去调用它:

  1. interface CallMeWithNewToGetString {
  2. new (): string;
  3. }
  4. // 使用
  5. declare const Foo: CallMeWithNewToGetString;
  6. const bar = new Foo(); // bar 被推断为 string 类型
  7. // 上述代码被编译成:
  8. var bar = new Foo(); // bar 被推断为 string 类型

用接口定义函数的形状

函数表达式使用接口定义函数的方式时,对等号左侧进行类型限制:

  1. interface SearchFunc {
  2. (source: string, subString: string): boolean;
  3. }
  4. let mySearch: SearchFunc = function(source, subString) {
  5. return source.search(subString) !== -1;
  6. }

可选参数

可选参数必须接在必需参数后面

  1. function buildName(firstName?: string, lastName: string) {
  2. if (firstName) {
  3. return firstName + ' ' + lastName;
  4. } else {
  5. return lastName;
  6. }
  7. }
  8. let tomcat = buildName('Tom', 'Cat');
  9. let tom = buildName(undefined, 'Tom');
  10. // 报错:必选参数不能位于可选参数后

必须要确保对象字面量在结构上类型兼容:

  1. function logIfHasName(something: { name?: string }) {
  2. if (something.name) {
  3. console.log(something.name);
  4. }
  5. }
  6. // 结构类型有一个缺点,它会误导你认为某些东西接收的数据比它实际的多
  7. logIfHasName({}); // okay
  8. logIfHasName({ name: 'matt' }); // okay
  9. logIfHasName({ name: 'cow', diet: 'vegan, but has milk of own species' }); // okay
  10. logIfHasName({ neme: 'I just misspelled name to neme' }); // Error

参数默认值

在 ES6 中,允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数,此时就不受「可选参数必须接在必需参数后面」的限制了:

  1. function buildName(firstName: string = 'Tom', lastName: string) {
  2. return firstName + ' ' + lastName;
  3. }
  4. let tomcat = buildName('Tom', 'Cat');
  5. let cat = buildName(undefined, 'Cat');

剩余参数

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

  1. // 事实上items 是一个数组。所以可以用数组的类型来定义
  2. function push(array: any[], ...items: any[]) {
  3. items.forEach(function(item) {
  4. array.push(item);
  5. });
  6. }
  7. let a = [];
  8. push(a, 1, 2, 3);

重载

重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。重载的方法必须写在真实的方法上面。
利用联合类型实现:缺点是不能够精确的表达,输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串

  1. function reverse(x: number | string): number | string | void {
  2. if (typeof x === 'number') {
  3. return Number(x.toString().split('').reverse().join(''));
  4. } else if (typeof x === 'string') {
  5. return x.split('').reverse().join('');
  6. }
  7. }

可以使用重载定义多个的函数类型:在编辑器的代码提示中,也可以看到正确的提示

  1. // 注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。
  2. function reverse(x: number): number;
  3. function reverse(x: string): string;
  4. function reverse(x: number | string): number | string | void {
  5. if (typeof x === 'number') {
  6. return Number(x.toString().split('').reverse().join(''));
  7. } else if (typeof x === 'string') {
  8. return x.split('').reverse().join('');
  9. }
  10. }
  11. reverse(123456)

image.png

枚举

枚举类似接口,只是隐藏了接口的key值
枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射:

  1. enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
  2. console.log(Days["Sun"] === 0); // true
  3. console.log(Days["Mon"] === 1); // true
  4. console.log(Days["Tue"] === 2); // true
  5. console.log(Days["Sat"] === 6); // true
  6. console.log(Days[0] === "Sun"); // true
  7. console.log(Days[1] === "Mon"); // true
  8. console.log(Days[2] === "Tue"); // true
  9. console.log(Days[6] === "Sat"); // true
  10. // 数字类型的枚举的实例允许被重新赋值,只要与数字类型相关就行!
  11. let day: Days.Sun = 1;
  12. let day: Days.Sun = '1'; // 报错:不能将类型“8”分配给类型“Days.Sun”。
  13. console.log(Days.Sun); // 0
  14. console.log(Days.Mon); // 1
  15. console.log(Days.Tue); // 2
  16. console.log(Days.Wed); // 3

手动赋值

未手动赋值的枚举项会接着上一个枚举项递增:

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

注意:未手动赋值的枚举项与手动赋值的重复,会被覆盖!

  1. enum Days {Sun = 3, Mon = 1, Tue, Wed, Thu, Fri, Sat};
  2. console.log(Days["Sun"] === 3); // true
  3. console.log(Days["Wed"] === 3); // true
  4. // TypeScript 并没有报错,导致 Days[3] 的值先是 "Sun",而后又被 "Wed" 覆盖了。
  5. console.log(Days[3] === "Sun"); // false
  6. console.log(Days[3] === "Wed"); // true

手动赋值的枚举项可以不是数字,此时需要使用类型断言来让 tsc 无视类型检查 (编译出的 js 仍然是可用的):

  1. // <any>"S" 是类型断言的语法
  2. enum Days {Sun = 7, Mon, Tue, Wed, Thu, Fri, Sat = <any>"S"};

手动赋值的枚举项也可以为小数或负数,此时后续未手动赋值的项的递增步长仍为 1:

  1. enum Days {Sun = 7, Mon = 1.5, Tue, Wed, Thu, Fri, Sat};
  2. console.log(Days["Sun"] === 7); // true
  3. console.log(Days["Mon"] === 1.5); // true
  4. console.log(Days["Tue"] === 2.5); // true
  5. console.log(Days["Sat"] === 6.5); // true

如果两个枚举的命名不相同,则它们类型不相等:

  1. // 创建一个只有名字的枚举;FOO
  2. enum FooIdBrand {
  3. _ = ''
  4. }
  5. type FooId = FooIdBrand & string;
  6. // 创建一个只有名字的枚举;BAR
  7. enum BarIdBrand {
  8. _ = ''
  9. }
  10. type BarId = BarIdBrand & string;
  11. let fooId: FooId;
  12. let barId: BarId;
  13. // 类型安全
  14. fooId = barId; // error
  15. barId = fooId; // error

常数项和计算所得项

枚举项有两种类型:常数项(constant member)和计算所得项(computed member)。
前面所举的例子都是常数项,一个典型的计算所得项的例子:

  1. enum Color {Red, Green, Blue = "blue".length}; // "blue".length 就是一个计算所得项。

上面的例子不会报错,但是如果紧接在计算所得项后面的是未手动赋值的项,那么它就会因为无法获得初始值而报错

  1. enum Color {Red = "red".length, Green, Blue}; // 报错:枚举成员必须具有初始化表达式。

异构枚举

异构枚举支持字符串,但是不支持反举

  1. enum Role {
  2. ADMIN = 'admin', // 支持字符串
  3. READ_ONLY = 1, // 下一个就必须要有初始值,才能进行推断
  4. AUTHOR,
  5. }
  6. // 但是字符串枚举不支持反举
  7. console.log(Role.ADMIN); // ok admin
  8. console.log(Role['admin']); // error,元素隐式具有"any"类型,因为索引表达式的类型不为"number"

常量枚举

常量枚举是使用 **const enum** 定义的枚举类型;常量枚举与普通枚举的区别是,它会在编译阶段被删除(可以获得性能提升),并且不能包含计算成员。支持字符串,但是不支持反举。

  1. const enum Tristate {
  2. False,
  3. True,
  4. Unknown
  5. }
  6. const lie = Tristate.False;
  7. // 上面编译后的结果:
  8. let lie = 0;

编译器将会:

  • 内联枚举的任何用法(0 而不是 Tristate.False);
  • 不会为枚举类型编译成任何 JavaScript(在这个例子中,运行时没有 Tristate 变量),因为它使用内联语法。

    常量枚举 preserveConstEnums 选项: 使用内联语法对性能有明显的提升作用。运行时没有 Tristate 变量的事实,是因为编译器把一些在运行时没有用到的不编译成 JavaScript。然而,想让编译器仍然把枚举类型编译成 JavaScript,用于如上例子中从字符串到数字,或者是从数字到字符串的查找。在这种情景下,可以使用编译选项--preserveConstEnums,它会编译出 var Tristate 的定义,因此在运行时,手动使用 Tristate[‘False’] 和 Tristate[0],并且这不会以任何方式影响内联。

当满足以下条件时,枚举成员被当作是常数:

  • 不具有初始化函数并且之前的枚举成员是常数。在这种情况下,当前枚举成员的值为上一个枚举成员的值加 1。但第一个枚举元素是个例外。如果它没有初始化方法,那么它的初始值为 0。

    1. // 包含了计算成员,则会在编译阶段报错:常量枚举成员初始值设定项只能包含文字值和其他计算的枚举值。
    2. const enum Color {Red, Green, Blue = "blue".length};
  • 枚举成员使用常数枚举表达式初始化。常数枚举表达式是 TypeScript 表达式的子集,它可以在编译阶段求值。当一个表达式满足下面条件之一时,它就是一个常数枚举表达式:

    • 数字字面量
    • 引用之前定义的常数枚举成员(可以是在不同的枚举类型中定义的)如果这个成员是在同一个枚举类型中定义的,可以使用非限定名来引用
    • 带括号的常数枚举表达式
    • +, -, ~ 一元运算符应用于常数枚举表达式
    • +, -, *, /, %, <<, >>, >>>, &, |, ^ 二元运算符,常数枚举表达式做为其一个操作对象。若常数枚举表达式求值后为 NaN 或 Infinity,则会在编译阶段报错

所有其它情况的枚举成员被当作是需要计算得出的值。