TypeScript - 接口详解

本文详细介绍了TypeScript接口的各种用法,包括可选属性、只读属性、额外属性检查及其应对策略、函数类型、可索引类型、class如何实现接口、继承接口、混合类型以及接口继承类。通过实例解析了如何利用接口进行类型检查,确保代码的健壮性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

TypeScript的核心原则之一是对值所具有的形态进行类型检查,在TypeScript中,接口的作用就是命名这些类型和在你的代码和第三方代码之间建立契约

举个栗子

function printLabel(labeledObj: { label: string}) {
    console.log(labeledObj.label);//类型检查检查printLabel的全部调用,传递的参数里面有没有label属性并且是string
}

let myObj = {size: 10, label: 'Size 10 Object'};

printLabel(myObj); //Size 10 Object
printLabel({size: 10, label: 2});//Error
printLabel({num: 2, label: 'size 10 Object'});//Error
let newObj = {num: 2, label: 'size 10 Object'};
printLabel(newObj);//Size 10 Object

用接口重写前个例子

interface LabeledValue {
    label: string;
}

function printLabel(labeledObj: LabeledValue) {
    console.log(labeledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

注:类型检查不关心属性出现的顺序,只要接口所需的属性存在并且类型正确。

可选属性(属性后加?)

开发中,不是所有属性都是每一次调用都必需的。这时就需要有可选属性在满足不同的条件

interface SquareConfig {
    color?: string,
    width?: number
}

function createSquare (config: SquareConfig) : {color: string; area: number}{ //注:此处的{color: string; area: number}是为函数createSquare定义类型。如果返回值不满足类型检查就会报错
    let newSquare = {color: 'white', area: 100};

    if(config.color){
        newSquare.color = config.color;
    }

    if(config.width){
        newSquare.area = config.width * config.width;
    }
    return newSquare;
}

let mySquare = createSquare({color: 'black'}); //{color: "black", area: 100}
let defaultSquare = createSquare({});//{color: "white", area: 100}

只读属性

写法:readOnly name : type

用于定义变量

interface Point {
    readonly x: number;
    readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; //error!

个人认为相当有用。目前就算把对象定义成const.依然是可以修改其属性。const只能用于定义变量。

const obj = {a: 2, c:3}; // {a: 2, c:3}
obj.c = 5;
obj//{a: 2, c: 5}

用于定义数组

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! 注:即便赋值给一个普通的数组也是不行的

只读数组可以通过类型断言重写

a = ro as number[];

**注意:只读属性只能选择不传,或者传相同属性名且符合类型检查的参数。

额外的属性检查

在第一个例子中只有传递满足{label: string}的参数才不会报错。在跟可选属性结合的时候就有点。。。

interface SquareConfig {
    color?: string,
    width?: number
}

function createSquare (config: SquareConfig) : {color: string; area: number}{
    let newSquare = {color: 'white', area: 100};

    if(config.color){
        newSquare.color = config.color;
    }

    if(config.width){
        newSquare.area = config.width * config.width;
    }
    return newSquare;
}

let mySquare = createSquare({ colour: "red", width: 100 });// error: 'colour' not expected in type 'SquareConfig'

TypeScript认为此代码中可能存在一个错误。 将对象文字分配给其他变量或将其作为参数传递时,将对其进行特殊处理并进行额外的属性检查。 如果对象文字具有“目标类型”所没有的任何属性,则会出现错误:

绕过这些检查非常简单

方法一、也是最简单的方式是运用类型断言

interface SquareConfig {
    color? : string,
    width? : number
}

function createSquare(config: SquareConfig) {
    let newSquare = {color: "white", area: 100};

    if(config.color){
        newSquare.color = config.color;
    }
    if(config.width){
        newSquare.area = config.width * config.width;
    }

    return newSquare;
}

let newSquad = createSquare({ colour: "red", width: 100 }); //Error
let newSquad = createSquare({ colour: "red", width: 100 } as SquareConfig); //会忽略不匹配的属性
console.log(newSquad); //{color: "white", area: 10000}

方法二、最好是给一个字符串索引签名. 当你确定这个对象在一些特殊的场景下可能会有其他额外的属性传入时。

interface SquareConfig {
    color?: string;
    width?: number;
    [propName: string]: any; //表示除开color和width以外,还可能会有任意数量的任意类型的属性
}
function createSquare(config: SquareConfig) {
    let newSquare = {color: "white", area: 100};

    if(config.color){
        newSquare.color = config.color;
    }
    if(config.width){
        newSquare.area = config.width * config.width;
    }

    return newSquare;
}
let newSquad = createSquare({ colour: "red", width: 100 });

console.log(newSquad); // {color: "white", area: 10000}

方法三、直接赋值给另一个变量

注:这种方法在赋值的时候必须带上一个公共的属性,例如例子里的width,否则也会报错

interface SquareConfig {
    color?: string;
    width?: number; 
}

function createSquare(config: SquareConfig) {
    let newSquare = {color: "white", area: 100};

    if(config.color){
        newSquare.color = config.color;
    }
    if(config.width){
        newSquare.area = config.width * config.width;
    }

    return newSquare;
}

let squareOptions = { colour: "red", width: 100 }; // squareOptions不会经过额外属性检查,所以编译器不会报错

let mySquare = createSquare(squareOptions); //{color: "white", area: 10000}

console.log(mySquare);

let newOption = { colour: "blue" };
let newSquare = createSquare(newOption); //Error

官方建议:对于简单的代码,不要试图通过以上三种方法绕开检查。对于具有方法和带有状态(state)的复杂对象常量。可以用这些技巧。但是大多数情况下,额外类型检查爆出来的都是bug. 你可能需要修改一些类型声明。针对这个例子,可以多添加一个colour的可选属性。

函数类型

给函数一个调用签名,一个函数声明只有参数列表和返回类型。在参数列表里面的每个参数都要求名字和类型。目的确保传入值的类型和返回值的类型是正确的

写法一:

a.定义一个接口
传入参数的类型 : 返回值的类型
interface searchFunc {
(source: string, subString: string): boolean
}

b.给函数定义接口
let myFunc : searchFunc;

c: 创建函数时,定义参数的个数和类型(可选步骤)
function (source: string, subString: string)
注:如果你不写参数的类型也是可以的,TypeScript会推断出参数类型。因为已经给函数赋值searchFunc类型变量。如果返回值不是true/false,类型检查会警告返回值跟接口定义的不匹配

interface searchFunc {
    (source: string, subString: string): boolean
}

let myFunc : searchFunc;

myFunc = function (source: string, subString: string) {
  let result = source.search(subString);
  return result > -1; //返回值不是true/false就会报错
};

Or

interface searchFunc {
    (source: string, subString: string): boolean
}

let myFunc : searchFunc;

myFunc = function (src, sub) { //函数的参数名字不作检查,可以任意写(src: string, sub: string)
  let result = src.search(sub);
  return result > -1; //返回值不是true/false就会报错
};

写法二 (函数那一节会详细讲解)

function myFunc(src: string, sub: string) : boolean {
    let result = src.search(sub);
    
    return result > 1;
}

可索引类型

跟我们运用接口描述函数类型一样,我们也可以用接口来描述索引的类型。例如a[10]; arg[‘daniel’]. 运用签名来描述索引的类型,和索引返回值的类型。

interface StringArray {
    [index: number]: string; //索引的类型是number, 索引返回值的类型是string
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

两种支持索引签名的类型:string 和number. 数字索引的返回值必须是字符串索引返回值类型的子类型这是因为当使用number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致

class Animal {
    name: string;
}
class Dog extends Animal {
    breed: string;
}

// 错误:使用string索引,有时会得到Animal!
interface NotOkay {
    [x: number]: Animal;
    [x: string]: Dog;
}

字符串索引签名可以很好的描述dictionary模式,并且它们也会确保所有的属性及它们的返回值类型匹配。因为string索引的声明通用于obj.property和obj[“property”]两种形式。

interface NumberDictionary {
    [index: string]: number;
    length: number;    //正确, length返回值类型是number
	name: string;      // Error, name的类型与索引类型返回值的类型不匹配
}
let numObj : NumberDictionary = {length: 12, width: 12, name: 12}; //Error,name的类型不匹配

最后,你也可以将索引签名设置为只读,防止给索引赋值

interface ReadonlyStringArray {
    readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!

class类型

实现一个接口

和C#, java语言里一样,TypeScript里面也可以强制要求class服从一个特殊的契约
(白话点说:实现接口就要在class里面实现接口里面的所有东西)

interface ClockInterface {
    currentTime: Date;
    setTime(d: Date): void //参数类型是Date, 无返回值
}

class Clock implements ClockInterface {
    currentTime = new Date();
    setTime (d: Date){
        this.currentTime = d;
    }
    constructor (h: number, m: number){}
}

注: 接口描述的是class的公共部分,因此不能用于检查类是否具有某些私有成员

类的静态部分与实力部分区别

interface ClockInterface {
    new (hour: number, minute: number);
}

class Clock implements ClockInterface {
    currentTime: Date;
    constructor(h: number, m: number) //静态部分
}

原因:当class实现一个接口时,只有class的实例部分是进行检查的,constructor属于静态部分,不在检查的范围内。所以报错。

  • Ps: 以下两个例子都没看懂。学完Class再回来细细研究 [Todo Item]

方法一:

interface ClockConstructor {
    new (hour: number, minute: number): ClockInterface;
  }
  interface ClockInterface {
    tick(): void;
  }
  
  function createClock(
    ctor: ClockConstructor,
    hour: number,
    minute: number
  ): ClockInterface {
    return new ctor(hour, minute);
  }
  
  class DigitalClock implements ClockInterface {
    constructor(h: number, m: number) {}
    tick() {
      console.log("beep beep");
    }
  }
  class AnalogClock implements ClockInterface {
    constructor(h: number, m: number) {}
    tick() {
      console.log("tick tock");
    }
  }
  
  let digital = createClock(DigitalClock, 12, 17);
  let analog = createClock(AnalogClock, 7, 32);

方法二

interface ClockConstructor {
    new (hour: number, minute: number);
  }
  
  interface ClockInterface {
    tick();
  }
  
  const Clock: ClockConstructor = class Clock implements ClockInterface {
    constructor(h: number, m: number) {}
    tick() {
      console.log("beep beep");
    }
  };

继承接口

继承单个接口

interface Shape {
  color: string;
}

interface Square extends Shape {
  sideLength: number;
}

let square = {} as Square;
square.color = "blue";
square.sideLength = 10;

继承多个接口

interface Shape {
  color: string;
}

interface PenStroke {
  penWidth: number;
}

interface Square extends Shape, PenStroke {
  sideLength: number;
}

let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

混合类型

用于当跟第三方js交互时,全面的描述类型

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;

接口继承类

当一个interface类型继承一个class类型,实际上继承的是class的成员而不是实现。就好比接口声明了该class的所有成员而没有提供实现。接口甚至继承了基类的私有成员和受保护成员。这意味着当你创建一个接口,并且这个接口继承了一个class的私有和受保护成员。那么这个接口就只能被该class和它的字类实现。

适用于当你拥有一个很大的继承层次结构,又要指定你的代码仅适用于具有某些属性的子类的时候。 子类除了从基类继承外,不必关联。

class Control {
    private state: any;
}

interface SelectableControl extends Control {
    select(): void;
}

class Button extends Control implements SelectableControl {
    select() { }
}

class TextBox extends Control {
    select() { }
}

// Error: Property 'state' is missing in type 'Image'.
class Image implements SelectableControl {
    private state: any;
    select() { }
}

class Location {

}

注:
Image类没有继承Control, 不是Control的子类。所以实现SelectableControl 接口会报错

  • PS:触及知识盲区; 个人认为私有变量不可继承,只能自己使用。但是官方手册写的是,可通过SelectableControl的实例访问私有成员state.看不懂。如有路过的大神,望解答 [Todo Item]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值