前言
众所周知,在传统的JavaScript中是没有接口
的概念的,所谓的接口
,其实就是描述集合属性的类型的一个特殊的虚拟结构。这也是开发一个大型项目所必须的语言特性,像Java、C#这样强类型语言,接口已经使用得非常广泛。于是,在TypeScrip中也引入了接口
的概念。
一、接口的基本使用
基与我们前面介绍的对象的类型的声明,可以定义一个函数的参数是包含特定属性的对象:
function doSomeThing(params: {name: string}):void {
console.log(params);
}
console.log(doSomeThing({name: '马松松'}));
// { name: '马松松' }
我们也可以使用接口
的方式实现上面的例子:
interface person {
name: string
}
function doSomeThing(params: person):void {
console.log(params);
}
console.log(doSomeThing({name: '马松松'}));
// { name: '马松松' }
两者是等效的,使用接口
的好处是可以将参数类型的配置抽离到一个单独的文件,这样使得项目更容易维护。
二、接口中使用可选参数
为了增强接口
的灵活性和延展性,TypeScript允许定义为接口类型的变量可以选择性匹配。
interface SquireParams {
width?: number,
height?: number
}
function squireResult(params: SquireParams):any {
let result: any;
if (params.width) {
result = params.width * params.width;
}
if (params.height) {
result = params.height * params.height;
}
if (params.width && params.height) {
result = params.width * params.height;
}
return result;
}
console.log(squireResult({height: 5}));
// 25
console.log(squireResult({width: 5}));
// 25
console.log(squireResult({width: 5,height: 5}));
// 25
当然,也可以和必选参数结合使用:
interface SquireParams {
width?: number,
height?: number,
label: string
}
function squireResult(params: SquireParams):any {
let result: any;
if (params.width) {
result = params.label + params.width * params.width;
}
if (params.height) {
result = params.label + params.height * params.height;
}
if (params.width && params.height) {
result = params.label + params.width * params.height;
}
return result;
}
console.log(squireResult({label: '计算结果为:', height: 5}));
// 计算结果为:25
三、接口中使用 只读属性
同时,在JavaScript中,没有关键字标识只读属性。我们可以通过Object.defineProperty
属性设置拦截,在TypeScript中明确提出了只读属性的关键字。
可以这样使用:
interface readonlyType {
readonly x: number,
readonly y: number
}
let readonlyObj: readonlyType = {x: 10, y: 10}
readonlyObj.x = 13;
//Cannot assign to 'x' because it is a read-only property
只允许初始化的时候,给x
和y
分配number
的值。
对于数组,TypeScript也提供了ReadonlyArray<T>这样的泛型只读数组,删除了该命名数组的操作数组的所有方法。
const arr: ReadonlyArray<number> = [1,2,3];
当你想往该数组推入一个数字时,会引发错误:
arr.push()
// Property 'push' does not exist on type 'readonly number[]'
⚠️对于const
和readonly的使用的场景:
TypeScript的官方推荐是:变量使用const,而属性使用readonly。
四、Excess Property Checks
这个是解决原生的JavaScript的行为和TypeScript行为不一致的方案,思考这样一个例子:
interface SquareConfig {
color ?: string,
width ?: number
}
function createSquare(config: SquareConfig): { color: string; area: number } {
return { color: config.color || "red", area: config.width ? config.width * config.width : 20 };
}
我们定义了一个SquareConfig
接口,然后作为函数的入参类型,然后我们这样使用这个函数:
let mySquare = createSquare({ colour: "red", width: 100 });
这里TypeScript会给出错误提示类型不匹配,但是按照我们之前说的可选参数的的例子,这里的color
并不是必须的,因为这里故意将color
拼成了colour
,TypeScript对以字面量方式定义对象的方式进行了特殊的类型检查处理,而在原生的JavaScript中是静默忽略的,为了避免出现这种情况,下面是几种更好的规避这种错误的方式:
1.使用as 强制推断类型
let mySquare = createSquare({colour: "red", width: 100} as SquareConfig);
2.不使用字面量的方式
let paramsSquare = {colour: "red", width: 100};
let mySecondSquare = createSquare(paramsSquare);
3.加一个额外的动态属性
interface SquareConfig {
color ?: string,
width ?: number,
[propName: string]: any;
}
let myThirdSquare = createSquare({colour: "red", width: 100});
当你想用传字面量的方式传入参数,为了规避不必要的错误,使用上面的几种方式就行。
五、在接口中定义 函数的参数类型和返回值类型
1.基本使用:
首先定义一个函数的接口,我们定义了参数的类型和返回值的类型
interface baseFunc {
(firstName: string, lastName: string): string
}
然后这样使用这个接口:
let myFunc: baseFunc = function (firstName: string, lastName: string) {
return firstName + lastName;
}
2.函数的入参不需要同名
let mySecondFunc: baseFunc = function (fName: string, lName: string) {
return fName + lName;
}
3.当你指定了函数签名的类型 函数的入参和返回值也不需要指明类型,类型系统会自动根据传入的参数推断类型
let myThirdFunc: baseFunc = function (fName, lName) {
return fName + lName;
}
4.但是如果你没有指定类型 但是返回了和接口返回类型不一致 类型检查不会通过
let myLastFunc: baseFunc = function (fName, lName) {
let result = fName + lName;
return 11;
}
六、接口中 定义数组和对象的索引类型
1.基本使用:
interface objectType {
[index: string]: string;
}
在对象中这样使用这个接口
let myObj: objectType = {name: '马松松', age: "18"};
可以看到,我们定义的索引是string
,属性值也是string
,所以这样定义是合理的。
但是如果将age
的属性定义为number
类型,就不符合我们接口的定义:
let myObj: objectType = {name: '马松松', age: 18}; // 这样是不符合接口的定义的
在数组中需要这样使用定义接口,数组的索引都是number
类型的:
interface arrayType {
[index: number]: string;
}
然后,你可以这样使用这个接口:
let myArr: arrayType = ["马松松","18"];
2.注意字符串索引和直接指定类型的方式一起使用的时候,字符串索引类型的优先级更高,所以直接指明属性的类型 需要保持和字符串索引一样.
interface numberDictionary {
[index: string]: number,
length: number,
// name: string // 这里使用string会报错,以为你字符串索引返回的类型是number
name: number, // 这样是可以的
}
3.那你确实想定义不同类型的属性 可以这样做
interface secondNumberDictionary {
[index: string]: number | string,
length: number,
name: string // 这样是可以的
}
4.也可以结合 readonly 定义只读属性
interface thirdNumberDistionary {
readonly [index: string]: string
}
// 此时当你想设置thirdNumberDistionary的属性的时候就会报错
let myThirdNumberDictionary: thirdNumberDistionary = {name: '马松松'};
// myThirdNumberDictionary.name = "宋志露"; // 不可设置
七、类和接口的关系
其他语言中使用接口做频繁的操作的就是,用类实现一个接口,从而使得类和接口缔结某种强制的联系。
1.基本使用:
我们首先定义一个日期接口:
interface BaseClock {
currentTime: string
}
使用implements
关键词缔结类和接口的契约关系:
class MyClock implements BaseClock {
currentTime: ""
constructor(h: number, m: number) {
}
}
缔结的契约关系为:MyClock
类中必须有类型为string
的currentTime
变量。
2.也可以缔结类中的方法的契约
先定义接口:
interface SecondBaseClock {
getCurrentTime(t: string): void
}
使用implements
缔结契约:
class MySecondClock implements SecondBaseClock {
getCurrentTime(t: string) {
this.currentTime = t;
}
}
缔结的契约关系为:MySecondClock
类中需要有一个getCurrentTime
方法,且需要一个类型为string
的入参,没有返回值。
3.在缔结类和接口的契约关系时 注意new关键词
当使用new
关键词实例化类时,TypeScript类型检查器不会检查静态类的构造器方法是否满足缔约,而是在你使用new关键词的时候判断是否满足。
比如我们定义一个构造函数的的接口:
interface C {
new (hour: number, min: number)
}
然后使用implements
缔结契约:
class Clock implements C {
constructor(h: number, m: number) {}
}
我们会得到报错信息:
Class 'Clock' incorrectly implements interface 'C'.Type 'Clock' provides no match for the signature 'new (hour: number, min: number): any'
我们缔结契约的类实际上是满足了构造函数的接口的,但是由于TypeScript类型检查不会直接检查类中构造函数是否满足契约,所以这里会报错。
所以正确的使用方式是将缔结契约的类赋值给别的变量,这样类型检查系统就会进行类型检查:
interface ClockInterface {
tick(): void
}
const Clock: ClockConstructor = class Clock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
}
}
这里注意这样的区别就好了。
八、接口中 使用继承
1.基本使用
我们首先定义一个Square
接口:
interface Square {
width: number,
height: number
}
然后这样使用:
let square = {} as Square;
square.width = 100;
square.height = 100;
为了使接口可以更灵活的构建更复杂的数据结构,这里使用到了extends
关键字:
interface baseSquare {
width: number,
}
interface Square extends baseSquare {
height: number
}
let square = {} as Square;
square.width = 100;
square.height = 100;
2.一个接口可以继承多个接口
interface baseFirstSquare {
width: number,
}
interface baseSecondSquare {
width: number,
}
然后我们可以同时继承这样两个接口:
class MySquare implements baseFirstSarare,baseSecondSquare {
color: string
}
九、接口中使用 混合类型
基于JavaScript语言的丰富性和灵活性,TypeScript允许使用混合类型
比如定义一个定时器接口:
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
然后你这样使用:
function getCounter(): Counter {
let counter = function (start: number) {} as Counter;
counter.interval = 123;
counter.reset = function () {};
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
这里使用as
推断了类型,就获取了一个对象,这里个人有点不理解。
九、接口继承
1.当继承的类是public时,可以直接实现这个接口
class Control {
public state: any
}
interface SelectableControl extends Control {
select(): void
}
这样使用:
let select: SelectableControl = {
state: 22,
select() {}
}
2.当继承的类private或者protected时 继承的接口只能通过被继承类子类去实现,不能直接实现
class SecondControl {
private state: any
}
interface SecondSelectableControl extends SecondControl {
select(): void
}
只能是被继承类的子类去实现该接口,因为只有被继承类的子类才能访问私有属性:
class MySecondSelectableControl extends SecondControl implements SecondSelectableControl {
select() {
}
}
然后你这样使用:
let s = new MySecondSelectableControl();
总结:接口的使用,其实也是引进了强类型语言的相关的概念,理解接口概念的同时,同时也能增强前端开发者对强类型语言和弱类型语言的特性的理解。