Typescript:高级类型
对象的类型
在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。
什么是接口
在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implements)。
TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。
interface Person { name: string; age: number; } let man: Person = { name: 'Xcat Liu', age: 25, };
上面的例子中,我们定义了一个接口 Person,接着定义了一个变量 man,它的类型是 Person。这样,我们就约束了 man的形状必须和接口 Person 一致。
接口一般首字母大写。
定义的变量比接口少了一些或者多了一些属性是不允许的:
interface Person { name: string; age: number; } let xcatliu: Person = { name: 'Xcat Liu', }; // index.ts(6,5): error TS2322: Type '{ name: string; }' is not assignable to type 'Person'. // Property 'age' is missing in type '{ name: string; }'.
可见,赋值的时候,变量的形状必须和接口的形状保持一致。
可选属性
有时我们希望不要完全匹配一个形状,那么可以用可选属性:
interface Person { name: string; age?: number; } let xcatliu: Person = { name: 'Xcat Liu', };
interface Person { name: string; age?: number; } let xcatliu: Person = { name: 'Xcat Liu', age: 25, };
可选属性的含义是该属性可以不存在。
这时仍然不允许添加未定义的属性:
interface Person { name: string; age?: number; } let xcatliu: Person = { name: 'Xcat Liu', age: 25, website: 'http://xcatliu.com', }; // examples/playground/index.ts(9,3): error TS2322: Type '{ name: string; age: number; website: string; }' is not assignable to type 'Person'. // Object literal may only specify known properties, and 'website' does not exist in type 'Person'.
任意属性
有时候我们希望一个接口允许有任意的属性,可以使用如下方式:
interface Person { name: string; age?: number; [propName: string]: any; } let xcatliu: Person = { name: 'Xcat Liu', website: 'http://xcatliu.com', };
使用 [propName: string] 定义了任意属性取 string 类型的值。
需要注意的是,一旦定义了任意属性,那么确定属性和可选属性都必须是它的子属性:
interface Person { name: string; age?: number; [propName: string]: string; } let xcatliu: Person = { name: 'Xcat Liu', age: 25, website: 'http://xcatliu.com', }; // index.ts(3,3): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'. // index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; website: string; }' is not assignable to type 'Person'. // Index signatures are incompatible. // Type 'string | number' is not assignable to type 'string'. // Type 'number' is not assignable to type 'string'.
上例中,任意属性的值允许是 string,但是可选属性 age 的值却是 number,number 不是 string 的子属性,所以报错了。
只读属性
有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性:
interface Person { readonly id: number; name: string; age?: number; [propName: string]: any; } let xcatliu: Person = { id: 89757, name: 'Xcat Liu', website: 'http://xcatliu.com', }; xcatliu.id = 9527; // index.ts(14,9): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.
上例中,使用 readonly 定义的属性 id 初始化后,又被赋值了,所以报错了。
只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候:
数组的类型
「类型 + 方括号」表示法
最简单的方法是使用「类型 + 方括号」来表示数组:
let fibonacci: number[] = [1, 1, 2, 3, 5];
数组的项中不允许出现其他的类型:
let fibonacci: number[] = [1, '1', 2, 3, 5]; // index.ts(1,5): error TS2322: Type '(number | string)[]' is not assignable to type 'number[]'. // Type 'number | string' is not assignable to type 'number'. // Type 'string' is not assignable to type 'number'.
数组泛型
也可以使用数组泛型(Generic) Array<elemType> 来表示数组:
let fibonacci: Array<number> = [1, 1, 2, 3, 5];
any 在数组中的应用
一个比较常见的做法是,用 any 表示数组中允许出现任意类型:
let list: any[] = ['Xcat Liu', 25, { website: 'http://xcatliu.com' }];
函数类型
函数声明
在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression):
// 函数声明(Function Declaration) function sum(x, y) { return x + y; } // 函数表达式(Function Expression) let mySum = function (x, y) { return x + y; };
一个函数有输入和输出,要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到,其中函数声明的类型定义较简单:
function sum(x: number, y: number): number { return x + y; }
注意,输入多余的(或者少于要求的)参数,是不被允许的
函数表达式
如果要我们现在写一个对函数表达式(Function Expression)的定义,可能会写成这样:
let mySum = function (x: number, y: number): number { return x + y; };
这是可以通过编译的,不过事实上,上面的代码只对等号右侧的匿名函数进行了类型定义,而等号左边的 mySum,是通过赋值操作进行类型推论而推断出来的。如果需要我们手动给 mySum 添加类型,则应该是这样:
let mySum: (x: number, y: number) => number = function (x: number, y: number): number { return x + y; };
在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。
接口中函数的定义
我们也可以使用接口的方式来定义一个函数需要符合的形状:
interface SearchFunc { (source: string, subString: string): boolean; } let mySearch: SearchFunc; mySearch = function(source: string, subString: string) { return source.search(subString) !== -1; }
可选参数
前面提到,输入多余的(或者少于要求的)参数,是不允许的。那么如何定义可选的参数呢?
与接口中的可选属性类似,我们用 ? 表示可选的参数:
function buildName(firstName: string, lastName?: string) { if (lastName) { return firstName + ' ' + lastName; } else { return firstName; } } let xcatliu = buildName('Xcat', 'Liu'); let xcat = buildName('Xcat');
需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必须参数了:
function buildName(firstName?: string, lastName: string) { if (firstName) { return firstName + ' ' + lastName; } else { return lastName; } } let xcatliu = buildName('Xcat', 'Liu'); let xcat = buildName(undefined, 'Xcat'); // index.ts(1,40): error TS1016: A required parameter cannot follow an optional parameter.
参数默认值
在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数:
function buildName(firstName: string, lastName: string = 'Liu') { return firstName + ' ' + lastName; } let xcatliu = buildName('Xcat', 'Liu'); let xcat = buildName('Xcat');
此时就不受「可选参数必须接在必需参数后面」的限制了:
function buildName(firstName: string = 'Xcat', lastName: string) { return firstName + ' ' + lastName; } let xcatliu = buildName('Xcat', 'Liu'); let xcat = buildName(undefined, 'Xcat');
剩余参数
ES6 中,可以使用 ...rest 的方式获取函数中的剩余参数(rest 参数):
function push(array, ...items) { items.forEach(function(item) { array.push(item); }); } let a = []; 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);
类型断言
类型断言(Type Assertion)可以用来绕过编译器的类型推断,手动指定一个值的类型(即程序员对编译器断言)。
<类型>值 // 或 值 as 类型
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:
function getLength(something: string | number): number { return something.length; } // index.ts(2,20): error TS2339: Property 'length' does not exist on type 'string | number'. // Property 'length' does not exist on type 'number'.
而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型的属性或方法,此时可以使用类型断言,将 something 断言成 string
function getLength(something: string | number): number { if ((<string>something).length) { return (<string>something).length; } else { return something.toString().length; } }