对象的类型——Interface
简介
在 TypeScript 中,使用接口(Interface)来定义对象的类型(对「对象的形状(Shape)」进行描述)。
在面向对象语言中,接口(Interface)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(class)去实现(implement)。
interface Girl {readonly id: number; // 只读属性name: string,age: number,bust?: number,// 可选属性[propName: string]: any, // 定义一个任意属性:key是string,值是anysay(): string // 该方法必须有返回值,且值为string}
注意:一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集:
interface Person {name: string;age?: number; // name报错:类型“number | undefined”的属性“age”不能赋给“string”索引类型“string”。job: undefined; // job报错:类型“undefined”的属性“job”不能赋给“string”索引类型“string”。[propName: string]: string;}let tom: Person = {name: 'Tom',age: 25,job: undefined,gender: 'male'};
解决:一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:
interface Person {name: string;age?: number;[propName: string]: string | number | undefined;}let tom: Person = {name: 'Tom',age: 25,gender: 'male'};
接口可以同名,同名的接口会进行合并(导入的接口不行)。type别名不行
interface IUser {name: string;}interface IUser {age: string;}const user: IUser = {name: 'John',age: '20',};
class实现interface
一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements 关键字来实现。一个类可以实现多个接口
interface human {eat(): void // 描述实例上的方法,或者原型上的方法}class Zbb implements Girl,human {constructor(id: number, name: string, age: number) {this.id = id;this.name = name;this.age = age;}id: number;name: string;age: number;say() {return "1234";}eat(): string { // ok,虽然接口描述的返回值是void,可以理解成是接口不关心返回值return '吃饭'}}
interface继承interface
interface Teacher extends Girl {teach(): string}
interface继承class
常见的面向对象语言中,接口是不能继承类的,但是在 TypeScript 中却是可以的(接口继承类,只会继承它的实例属性和实例方法。):
class Point {/** 静态属性,坐标系原点 */static origin = new Point(0, 0);/** 静态方法,计算与原点距离 */static distanceToOrigin(p: Point) {return Math.sqrt(p.x * p.x + p.y * p.y);}/** 实例属性,x 轴的值 */x: number;/** 实例属性,y 轴的值 */y: number;/** 构造函数 */constructor(x: number, y: number) {this.x = x;this.y = y;}/** 实例方法,打印此点 */printPoint() {console.log(this.x, this.y);}}// interface PointInstanceType {// x: number;// y: number;// printPoint(): void;// }// 等价于 interface Point3d extends PointInstanceType {interface Point3d extends Point {z: number;}let point3d: Point3d = {x: 1, y: 2, z: 3,printPoint: function (): void {throw new Error('Function not implemented.');}};
解析:实际上,当声明 class Point 时,除了会创建一个名为 Point 的类之外,同时也创建了一个名为 Point 的类型(实例的类型)。所以既可以将 Point 当做一个类来用(使用 new Point 创建它的实例),也可以将 Point 当做一个类型来用(使用 : Point 表示参数的类型)。
所以「接口继承类」和「接口继承接口」没有什么本质的区别。值得注意的是,PointInstanceType 相比于 类Point,不包含constructor 方法、静态属性或静态方法(实例的类型当然不应该包括构造函数、静态属性或静态方法)。
数组的类型
在 TypeScript 中,数组类型定义主要有两种方式:「类型 + 方括号」表示法 和 数组泛型
[类型 + 方括号]表示法
let fibonacci: number[] = [1, 1, 2, 3, 5];
数组泛型
let fibonacci: Array<number> = [1, 1, 2, 3, 5];
用接口表示数组
虽然接口也可以用来描述数组,但是一般不会这么做,因为这种方式比前两种方式复杂多了:
interface NumberArray {[index: number]: number;}let fibonacci: NumberArray = [1, 1, 2, 3, 5];
接口表示数组常用来表示类数组,常用的类数组TypeScript中的内置对象都有自己的定义,如 IArguments, NodeList, HTMLCollection等:
interface IArguments {[index: number]: any;length: number;callee: Function;}
元组
数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象,超出元组固定长度的越界元素,它的类型会被限制为元组中每个类型的联合类型(主要针对push方法,修改索引下标不行)。
let tuple: [string, number] = ['a', 1] // 初始化的时候必须提供所有元组类型中指定的项。tuple[0] = 'Tom'; // oktuple[1] = 25; // oktuple[2] = 25; // error,不能将类型“25”分配给类型“undefined”tuple[2] = undefined; // error,长度为"2"的元组类型"[string, number]"在索引"2"处没有元素tuple[0].slice(1); // oktuple[1].toFixed(2); // oktuple.push(1); // ok ⭐只要push进去的值类型属于元组中每个类型的联合类型即可tuple.push(true); // error,类型“boolean”的参数不能赋给类型“string | number”的参数
内置对象
JavaScript 中有很多内置对象,可以直接在 TypeScript 中当做定义好了的类型使用。而他们的定义文件,则在 TypeScript 核心库的定义文件中。
ECMAScript 的内置对象
ECMAScript 标准提供的内置对象有:Boolean、Error、Date、RegExp等
let b: Boolean = new Boolean(1);let e: Error = new Error('Error occurred');let d: Date = new Date();let r: RegExp = /[a-z]/;
DOM 和 BOM 的内置对象
DOM 和 BOM 提供的内置对象有:Document、HTMLElement、Event、NodeList等
let body: HTMLElement = document.body;let allDiv: NodeList = document.querySelectorAll('div');document.addEventListener('click', function(e: MouseEvent) {// Do something});
TypeScript 核心库的定义文件
TypeScript 核心库的定义文件中定义了所有浏览器环境需要用到的类型,并且是预置在 TypeScript 中的。
在使用一些常用的方法的时候,TypeScript 实际上已经做了很多类型判断的工作了,比如:
Math.pow(10, '2');// 报错:类型“string”的参数不能赋给类型“number”的参数。
解析:事实上 Math.pow 的类型定义如下:必须接受两个 number 类型的参数
interface Math {/*** Returns the value of a base expression taken to a specified power.* @param x The base value of the expression.* @param y The exponent value of the expression.*/pow(x: number, y: number): number;}
但是Node.js 不是内置对象的一部分,如果想用 TypeScript 写 Node.js,则需要引入第三方声明文件:**npm install @types/node --save-dev**
函数的类型
在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression)。
函数声明
function sum(x: number, y: number): number {return x + y;}
函数表达式
let mySum = (x: number, y: number): number => {return x + y;};// 等号右边定义了,左边通过类型推论就可以简化
上面的代码也可以通过编译,但是它只对等号右侧的匿名函数进行了类型定义,而等号左边的 mySum,是通过赋值操作进行类型推论而推断出来的。如果需要手动给 mySum 添加类型,应该是这样:
// 等号两边同时定义let mySum: (x: number, y: number) => number = (x: number, y: number): number => {return x + y;};// 左边定义了右边就可以简化let mySum: (x: number, y: number) => number = (x, y) => {return x + y;};
注意不要混淆了 TypeScript 中的 => 和 ES6 中的 =>。
TypeScript中的**=>** 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。
type声明函数类型
在没有提供函数实现的情况下,type有两种声明函数类型的方式:
type LongHand = {(a: number): number;};type ShortHand = (a: number) => number;
但是想使用函数重载时,只能用第一种方式:
type LongHandAllowsOverloadDeclarations = {(a: number): number;(a: string): string;};
interface声明函数类型
// 表示一个返回值为 string 的函数interface ReturnString {(): string;}// 可以根据实际来传递任何参数、可选参数以及 rest 参数interface Complex {(foo: string, bar?: number, ...others: boolean[]): number;}// 函数重载interface Overloaded {(foo: string): string;(foo: number): number;}// 实现接口的一个例子:function stringOrNumber(foo: number): number;function stringOrNumber(foo: string): string;function stringOrNumber(foo: any): any {if (typeof foo === 'number') {return foo * foo;} else if (typeof foo === 'string') {return `hello ${foo}`;}}const overloaded: Overloaded = stringOrNumber;// 使用const str = overloaded(''); // str 被推断为 'string'const num = overloaded(123); // num 被推断为 'number'
可实例化:使用 new 作为前缀,意味着需要使用 new 关键字去调用它:
interface CallMeWithNewToGetString {new (): string;}// 使用declare const Foo: CallMeWithNewToGetString;const bar = new Foo(); // bar 被推断为 string 类型// 上述代码被编译成:var bar = new Foo(); // bar 被推断为 string 类型
用接口定义函数的形状
函数表达式使用接口定义函数的方式时,对等号左侧进行类型限制:
interface SearchFunc {(source: string, subString: string): boolean;}let mySearch: SearchFunc = function(source, subString) {return source.search(subString) !== -1;}
可选参数
可选参数必须接在必需参数后面:
function buildName(firstName?: string, lastName: string) {if (firstName) {return firstName + ' ' + lastName;} else {return lastName;}}let tomcat = buildName('Tom', 'Cat');let tom = buildName(undefined, 'Tom');// 报错:必选参数不能位于可选参数后
必须要确保对象字面量在结构上类型兼容:
function logIfHasName(something: { name?: string }) {if (something.name) {console.log(something.name);}}// 结构类型有一个缺点,它会误导你认为某些东西接收的数据比它实际的多logIfHasName({}); // okaylogIfHasName({ name: 'matt' }); // okaylogIfHasName({ name: 'cow', diet: 'vegan, but has milk of own species' }); // okaylogIfHasName({ neme: 'I just misspelled name to neme' }); // Error
参数默认值
在 ES6 中,允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数,此时就不受「可选参数必须接在必需参数后面」的限制了:
function buildName(firstName: string = 'Tom', lastName: string) {return firstName + ' ' + lastName;}let tomcat = buildName('Tom', 'Cat');let cat = buildName(undefined, 'Cat');
剩余参数
ES6 中,可以使用 …rest 的方式获取函数中的剩余参数(rest 参数):
// 事实上items 是一个数组。所以可以用数组的类型来定义function push(array: any[], ...items: any[]) {items.forEach(function(item) {array.push(item);});}let a = [];push(a, 1, 2, 3);
重载
重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。重载的方法必须写在真实的方法上面。
利用联合类型实现:缺点是不能够精确的表达,输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串:
function reverse(x: number | string): number | string | void {if (typeof x === 'number') {return Number(x.toString().split('').reverse().join(''));} else if (typeof x === 'string') {return x.split('').reverse().join('');}}
可以使用重载定义多个的函数类型:在编辑器的代码提示中,也可以看到正确的提示
// 注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。function reverse(x: number): number;function reverse(x: string): string;function reverse(x: number | string): number | string | void {if (typeof x === 'number') {return Number(x.toString().split('').reverse().join(''));} else if (typeof x === 'string') {return x.split('').reverse().join('');}}reverse(123456)
枚举
枚举类似接口,只是隐藏了接口的key值
枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射:
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};console.log(Days["Sun"] === 0); // trueconsole.log(Days["Mon"] === 1); // trueconsole.log(Days["Tue"] === 2); // trueconsole.log(Days["Sat"] === 6); // trueconsole.log(Days[0] === "Sun"); // trueconsole.log(Days[1] === "Mon"); // trueconsole.log(Days[2] === "Tue"); // trueconsole.log(Days[6] === "Sat"); // true// 数字类型的枚举的实例允许被重新赋值,只要与数字类型相关就行!let day: Days.Sun = 1;let day: Days.Sun = '1'; // 报错:不能将类型“8”分配给类型“Days.Sun”。console.log(Days.Sun); // 0console.log(Days.Mon); // 1console.log(Days.Tue); // 2console.log(Days.Wed); // 3
手动赋值
未手动赋值的枚举项会接着上一个枚举项递增:
enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat};console.log(Days["Sun"] === 7); // trueconsole.log(Days["Mon"] === 1); // trueconsole.log(Days["Tue"] === 2); // trueconsole.log(Days["Sat"] === 6); // true
注意:未手动赋值的枚举项与手动赋值的重复,会被覆盖!
enum Days {Sun = 3, Mon = 1, Tue, Wed, Thu, Fri, Sat};console.log(Days["Sun"] === 3); // trueconsole.log(Days["Wed"] === 3); // true// TypeScript 并没有报错,导致 Days[3] 的值先是 "Sun",而后又被 "Wed" 覆盖了。console.log(Days[3] === "Sun"); // falseconsole.log(Days[3] === "Wed"); // true
手动赋值的枚举项可以不是数字,此时需要使用类型断言来让 tsc 无视类型检查 (编译出的 js 仍然是可用的):
// <any>"S" 是类型断言的语法enum Days {Sun = 7, Mon, Tue, Wed, Thu, Fri, Sat = <any>"S"};
手动赋值的枚举项也可以为小数或负数,此时后续未手动赋值的项的递增步长仍为 1:
enum Days {Sun = 7, Mon = 1.5, Tue, Wed, Thu, Fri, Sat};console.log(Days["Sun"] === 7); // trueconsole.log(Days["Mon"] === 1.5); // trueconsole.log(Days["Tue"] === 2.5); // trueconsole.log(Days["Sat"] === 6.5); // true
如果两个枚举的命名不相同,则它们类型不相等:
// 创建一个只有名字的枚举;FOOenum FooIdBrand {_ = ''}type FooId = FooIdBrand & string;// 创建一个只有名字的枚举;BARenum BarIdBrand {_ = ''}type BarId = BarIdBrand & string;let fooId: FooId;let barId: BarId;// 类型安全fooId = barId; // errorbarId = fooId; // error
常数项和计算所得项
枚举项有两种类型:常数项(constant member)和计算所得项(computed member)。
前面所举的例子都是常数项,一个典型的计算所得项的例子:
enum Color {Red, Green, Blue = "blue".length}; // "blue".length 就是一个计算所得项。
上面的例子不会报错,但是如果紧接在计算所得项后面的是未手动赋值的项,那么它就会因为无法获得初始值而报错:
enum Color {Red = "red".length, Green, Blue}; // 报错:枚举成员必须具有初始化表达式。
异构枚举
异构枚举支持字符串,但是不支持反举
enum Role {ADMIN = 'admin', // 支持字符串READ_ONLY = 1, // 下一个就必须要有初始值,才能进行推断AUTHOR,}// 但是字符串枚举不支持反举console.log(Role.ADMIN); // ok adminconsole.log(Role['admin']); // error,元素隐式具有"any"类型,因为索引表达式的类型不为"number"
常量枚举
常量枚举是使用 **const enum** 定义的枚举类型;常量枚举与普通枚举的区别是,它会在编译阶段被删除(可以获得性能提升),并且不能包含计算成员。支持字符串,但是不支持反举。
const enum Tristate {False,True,Unknown}const lie = Tristate.False;// 上面编译后的结果:let lie = 0;
编译器将会:
- 内联枚举的任何用法(0 而不是 Tristate.False);
- 不会为枚举类型编译成任何 JavaScript(在这个例子中,运行时没有 Tristate 变量),因为它使用内联语法。
常量枚举 preserveConstEnums 选项: 使用内联语法对性能有明显的提升作用。运行时没有 Tristate 变量的事实,是因为编译器把一些在运行时没有用到的不编译成 JavaScript。然而,想让编译器仍然把枚举类型编译成 JavaScript,用于如上例子中从字符串到数字,或者是从数字到字符串的查找。在这种情景下,可以使用编译选项
--preserveConstEnums,它会编译出 var Tristate 的定义,因此在运行时,手动使用 Tristate[‘False’] 和 Tristate[0],并且这不会以任何方式影响内联。
当满足以下条件时,枚举成员被当作是常数:
不具有初始化函数并且之前的枚举成员是常数。在这种情况下,当前枚举成员的值为上一个枚举成员的值加 1。但第一个枚举元素是个例外。如果它没有初始化方法,那么它的初始值为 0。
// 包含了计算成员,则会在编译阶段报错:常量枚举成员初始值设定项只能包含文字值和其他计算的枚举值。const enum Color {Red, Green, Blue = "blue".length};
枚举成员使用常数枚举表达式初始化。常数枚举表达式是 TypeScript 表达式的子集,它可以在编译阶段求值。当一个表达式满足下面条件之一时,它就是一个常数枚举表达式:
- 数字字面量
- 引用之前定义的常数枚举成员(可以是在不同的枚举类型中定义的)如果这个成员是在同一个枚举类型中定义的,可以使用非限定名来引用
- 带括号的常数枚举表达式
- +, -, ~ 一元运算符应用于常数枚举表达式
- +, -, *, /, %, <<, >>, >>>, &, |, ^ 二元运算符,常数枚举表达式做为其一个操作对象。若常数枚举表达式求值后为 NaN 或 Infinity,则会在编译阶段报错
