typescript 初探之 interface(接口)

这篇博客详细介绍了TypeScript中的interface接口,包括对函数、类的使用,类静态部分与实例部分的区别,接口继承,混合类型以及接口继承类的情况。通过示例解释了接口如何约束对象类型,如可选属性、any类型、索引签名和只读属性的使用,以及接口描述函数和类的方法。同时,文章强调了接口继承类时只会检查实例部分,并不会包含实现。

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

typescript 接口用于约束变量的类型,名称与需要验证的变量一一对应,它不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以,可复用。它的结构是interface 接口名称 {定义规则}

interface Test {
  price: number;
  name: string;
}
let info: Test = {
  price: 10,
  name: "Apple"
};
function messa(arg): void {
  console.log(arg); // { price: 10, name: 'Apple' }
}
messa(info);

上述 Test 接口对 info 的数据类型做了限制,名称一一对应,price 为 number 类型、name 为字符串类型。当类型定义不为可选类型和 any 时,这些在 Test 里出现的变量都必须出现在需要被检测的对象上,变量类型要对应上:

interface Test {
  price: number;
  name: string;
}
let info: Test = {
  name: "Apple" // 不定义price或者类型不对则报错
};

如果我们不确定某属性会不会出现,或者不确定类型,则可以用可选类型和 any 类型代替:

interface Test {
  price?: number;
  name: string;
  size: any;
}
let info: Test = {
  name: "Apple",
  size: "12"
};

上述的例子 price 是可选类型可有可无,size 是 any 类型,size 的值可以是任意数据类型。
如果我们并不是需要让属性的类型可选,而是让属性可选,那么我们可以使用索引签名。照上面普通写法,当我们给info添加一个 Test 接口里没有定义的属性时就会报错:

interface Test {
  price?: number;
  name: string;
}
let info: Test = {
  price: 10,
  name: "Apple"
};

info.addData = 20; // 类型“Test”上不存在属性“addData”。

如果我们使用字符串索引签名,则可以给 info 添加没有被定义类型的属性:

interface Test {
  price?: number;
  name: string;
  [index: string]: any;
}
let info: Test = {
  price: 10,
  name: "Apple"
};

info.addData = 20; // 可以被添加

索引类型描述能够通过索引得到的类型,也就是能够通过下标找到值。比如数组 A:[‘a’.‘b’]可以通过 A[0]获取’a’(字符串也可以通过索引获取值,例如 B:‘abcd’可以通过 B[0]获取’a’)。索引类型支持两种索引签名:字符串和数字:

[index:number]:number
[index:string]:number

如果同时使用两种类型索引,数字索引所作用的对象必须是字符串索引作用对象的子类:

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

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

官网:因为当使用 number 来索引时,JavaScript 会将它转换成 string 然后再去索引对象。 也就是说用 100(一个 number)去索引等同于使用"100"(一个 string)去索引,因此两者需要保持一致。

可以使用readonly来设置一个对象是否是只读属性。设置只读属性后的变量只能被使用不能被修改,(对可选属性设置只读也是一样,一旦对某个对象设置了readonly那个对象不能被修改或者增加到对象上)例如:

interface Test {
  readonly price: number;
  name: string;
}
let info: Test = {
  price: 10,
  name: "Apple"
};

info.price = 20;    // Cannot assign to 'price' because it is a read-only property

typescript 具有ReadonlyArray<T>类型,它与 Array相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改,就算把整个 ReadonlyArray 赋值到一个普通数组也是不可以的:

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 类型数组直接不能赋值赋值给变量,但可以使用类型断言重写:

a = ro as number[];

readonly 和 const
最简单判断该用 readonly 还是 const 的方法是看要把它做为变量使用还是做为一个属性。做为变量使用的话用 const,若做为属性则使用 readonly。

对函数使用接口

接口除了对一些普通对象描述,还可以描述函数。格式:(名称:类型):类型,()中代表要描述的函数参数,:后的代表描述函数返回值类型。例如:

interface Test {
  (number1: number, number2: number): boolean;
}
let info: Test = function(number1: number, number2: number) {
  return number1 > number2;
};

我们也可以这么写:

interface Test {
  (number1: number, number2: number): boolean;
}
let info: Test = function(number1, number2) {
  return number1 > number2;
};

函数的参数名称不一定要和接口定义的名称一致,typescript 会对函数的参数逐个进行检查,要求对应位置参数类型一致,不过调用方法时的参数顺序(类型)得和接口对应上:

interface Test {
  (number1: number, number2: string): string;
}
let info: Test = function(n1, n2) {
  return n1 + n2;
};

console.log(info(1, "3")); //13
对类使用接口

接口也可以对类进行描述,和普通对象接口差不多,可以对类里要实现的方法进行描述。一样的套路。看官网的例子:

interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}

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

接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。

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

类具有两个类型:静态部分的类型和实例的类型。 如果用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误:

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

class Clock implements ClockConstructor {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

因为当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor 存在于类的静态部分,所以不在检查的范围内。

因此,我们应该直接操作类的静态部分。 看下面的例子,我们定义了两个接口,ClockConstructor 为构造函数所用和 ClockInterface 为实例方法所用。 为了方便我们定义一个构造函数 createClock,它用传入的类型创建实例。

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

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);

因为 createClock 的第一个参数是 ClockConstructor 类型,在 createClock(AnalogClock, 7, 32)里,会检查 AnalogClock 是否符合构造函数签名。

继承接口

和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。


interface Shape {
    color: string;
}

interface Square extends Shape {
    sideLength: number;
}

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

一个接口可以继承多个接口,创建出多个接口的合成接口。

interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

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

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
混合类型

接口能够描述 JavaScript 里的类型。 因为 JavaScript 其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。

一个例子就是,一个对象可以同时做为函数和对象使用,并带有额外的属性。

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

function getCounter(): Counter {
    let counter = <Counter>function (start: number) { };
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

在使用 JavaScript 第三方库的时候,你可能需要像上面那样去完整地定义类型。

接口继承类

当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的 private 和 protected 成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。

当你有一个庞大的继承结构时,要指出代码只在子类拥有特定属性时起作用。 这个子类除了继承至基类外与基类没有任何关系。 例:

class Control {
    private state: any;
}

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

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

class TextBox extends Control {

}

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

class Location {

}

在上面的例子里,SelectableControl 包含了 Control 的所有成员,包括私有成员state。 因为state是私有成员,所以只能够是 Control 的子类们才能实现 SelectableControl 接口。 因为只有 Control 的子类才能够拥有一个声明于 Control 的私有成员state,这对私有成员的兼容性是必需的。

在 Control 类内部,是允许通过 SelectableControl 的实例来访问私有成员state的。 实际上,SelectableControl 就像 Control 一样,并拥有一个 select 方法。 Button 和 TextBox 类是 SelectableControl 的子类(因为它们都继承自 Control 并有 select 方法),但 Image 和 Location 类并不是这样的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值