TS -- (2)接口、数组的类型、函数的类型
2019-10-28:
学习内容:接口、数组的类型、函数的类型
(展开涉及多个内容)
参考:https://ts.xcatliu.com/basics/type-of-function
一、接口(Interfaces):
(1)
LabelledValue
接口就好比一个名字,用来描述上面例子里的要求。 它代表了有一个 label
属性且类型为string
的对象。 需要注意的是,我们在这里并不能像在其它语言里一样,说传给 printLabel
的对象实现了这个接口。我们只会去关注值的外形。 只要传入的对象满足上面提到的必要条件,那么它就是被允许的。
还有一点值得提的是,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。
interface LabelledValue { label: string;
color?: string } function printLabel(labelledObj: LabelledValue) { console.log(labelledObj.label); } let myObj = {size: 10, label: "Size 10 Object"}; printLabel(myObj);
// 可选属性:接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。在可选属性名字定义的后面加一个?
符号。可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误
(2)只读属性:readonly
一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly
来指定只读属性
interface Point { readonly x: number; readonly y: number; } let p1: Point = { x: 10, y: 20 }; p1.x = 5; // error!
只读数组类型:
let a: number[] = [1, 2, 3, 4]; let ro: ReadonlyArray<number> = a; ro[0] = 12; // error! ro.push(5); // error! ro.length = 100; // error! a = ro; // error! // 最后一行可以看到就算把整个ReadonlyArray赋值到一个普通数组也是不可以的。解决的方法就是类型断言重写(就是没有办法)
区别readonly 和 const:const针对变量,readonly针对属性。
(3)额外的属性检查:多数情况下它都是个bug,不建议绕开检查
interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): { color: string; area: number } { // ... } let mySquare = createSquare({ colour: "red", width: 100 });
//error: 'colour' not expected in type 'SquareConfig'
注意传入createSquare
的参数拼写为colour
而不是color
。 在JavaScript里,这会默默地失败。对象字面量会被特殊对待而且会经过 额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。
-- 如何绕开检查?
解决办法一:
最佳的方式是能够添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。 如果 SquareConfig
带有上面定义的类型的color
和width
属性,并且还会带有任意数量的其它属性,那么我们可以这样定义它:
interface SquareConfig { color?: string; width?: number; [propName: string]: any; }
解决办法二:
将这个对象赋值给一个另一个变量: 因为 squareOptions
不会经过额外属性检查,所以编译器不会报错。
let squareOptions = { colour: "red", width: 100 }; let mySquare = createSquare(squareOptions);
(4)用接口描述函数类型:(输入输出判断)
为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。
interface SearchFunc { (source: string, subString: string): boolean; } let mySearch: SearchFunc; mySearch = function(src: string, sub: string): boolean { let result = src.search(sub); return result > -1; } // 函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。 如果你不想指定类型,TypeScript的类型系统会推断出参数类型,因为函数直接赋值给了 SearchFunc类型变量。 函数的返回值类型是通过其返回值推断出来的(此例是 false和true)。 如果让这个函数返回数字或字符串,类型检查器会警告我们函数的返回值类型与 SearchFunc接口中的定义不匹配。
(5)可索引的类型:
可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。
interface StringArray { [index: number]: string; } // 索引签名 let myArray: StringArray; myArray = ["Bob", "Fred"]; let myStr: string = myArray[0]; // 我们定义了StringArray接口,它具有索引签名。 这个索引签名表示了当用 number去索引StringArray时会得到string类型的返回值。
TypeScript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number
来索引时,JavaScript会将它转换成string
然后再去索引对象。 也就是说用 100
(一个number
)去索引等同于使用"100"
(一个string
)去索引,因此两者需要保持一致。
字符串索引签名能够很好的描述dictionary
模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了 obj.property
和obj["property"]
两种形式都可以
interface NumberDictionary { [index: string]: number; length: number; // 可以,length是number类型。数字索引的返回值必须是字符串索引返回值类型的子类型 name: string // 错误,`name`的类型与索引类型返回值的类型不匹配,应该是number 类型 }
最后,你可以将索引签名设置为只读,这样就防止了给索引赋值:
interface ReadonlyStringArray { readonly [index: number]: string; } let myArray: ReadonlyStringArray = ["Alice", "Bob"]; myArray[2] = "Mallory"; // error! // 你不能设置myArray[2],因为索引签名是只读的。
(6)类类型,继承接口:将在《类与接口》部分补充
二、数组的类型:
回顾:类型+方括号表示法:number[]
数组泛型: Array<number>
(1)用接口表示数组:
NumberArray
表示:只要索引的类型是数字时,那么值的类型必须是数字。
虽然接口也可以用来描述数组,但是我们一般不会这么做,因为这种方式比前两种方式复杂多了。类数组除外
interface NumberArray {
[index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];
(2)类数组:
类数组不是数组类型!
下例除了约束当索引的类型是数字时,值的类型必须是数字之外,也约束了它还有 length
和 callee
两个属性
function sum() { let args: { [index: number]: number; length: number; callee: Function; } = arguments; }
内置对象有IArguments
, NodeList
, HTMLCollection是TS定义好的类型,有自己的接口定义:
function sum() { let args: IArguments = arguments; } interface IArguments { [index: number]: any; length: number; callee: Function; } // Type 'IArguments' is missing the following properties from type 'number[]': pop, push, concat, join, and 24 more.
(3)any在数组中的应用:
数组中允许出现任意类型:
let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];
三、函数的类型:(JS对函数式的支持更好)
(1)函数的声明有两种:
1、函数声明:
// 函数声明(Function Declaration) function sum(x, y) { return x + y; }
2、函数表达式:
// 函数表达式(Function Expression) let mySum = function (x, y) { return x + y; };
(2)一个函数有输入和输出,要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到:
函数声明的约束:
function sum(x: number, y: number): number { return x + y; }
注意:不允许超出或少于数量的参数
函数表达式的约束:
let mySum: (x: number, y: number) => number = function (x: number, y: number): number { return x + y; }; // 对等号右侧的匿名函数进行了类型定义,而等号左边的 mySum,是通过赋值操作进行类型推论而推断出来的。手动给等号左边添加类型应该这样
注意:不要混淆了 TypeScript 中的 =>
和 ES6 中的 =>
。
在 TypeScript 的类型定义中,=>
用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。
在 ES6 中,=>
叫做箭头函数,应用十分广泛,
(3)用接口定义函数的形状,见《接口》部分第三part
(4)函数中的可选参数,见《接口》第一part《可选参数》
需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了
(5)参数默认值:
在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数,此时就不受「可选参数必须接在必需参数后面」的限制了:
function buildName(firstName: string = 'Tom', lastName: string) { return firstName + ' ' + lastName; } let tomcat = buildName('Tom', 'Cat'); let cat = buildName(undefined, 'Cat');
(6)剩余参数的获取:
使用 ...rest
的方式获取函数中的剩余参数(rest 参数):
function push(array: any[], ...items: any[]) { items.forEach(function(item) { array.push(item); }); } let a = []; push(a, 1, 2, 3);
(7)重载:
重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。
比如,我们需要实现一个函数 reverse
,输入数字 123
的时候,输出反转的数字 321
,输入字符串 'hello'
的时候,输出反转的字符串 'olleh'。
这时,我们可以使用重载定义多个 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(''); } }
我们重复定义了多次函数 reverse
,前几次都是函数定义,最后一次是函数实现。在编辑器的代码提示中,可以正确的看到前两个提示。
注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。