属性或方法的修饰符

在对象类型中,不仅可以为每个属性指定名称及类型,还可以用修饰符为此属性赋予特定的性质。属性修饰符主要有 3 种——可选修饰符、只读修饰符和索引签名。接下来,将一一介绍。

可选修饰符

默认情况下,对象类型字面量或接口中定义了多少个属性和方法,在使用它创建具体对象时就需要传入多少个属性和方法,而且类型必须符合其定义,否则会引起编译错误,示例代码如下。

interface CalculationOf2Numbers {
    num1: number,
    num2: number,
    calculate: () => number
}

let calculator: CalculationOf2Numbers;

//编译错误:类型"{ num1: number; }"缺少类型"CalculationOf2Numbers"中的以下属性: num2,
//calculatets(2739)
calculator = { num1: 1 };

但在一些情况下,某些属性或方法可能并不是必需的,因此就需要使用可选修饰符。在 TypeScript 中,通过在属性或方法后添加问号将其定义为可选的,然后在创建具体对象或赋值时,这些可选属性或方法可有可无。例如,以下代码均能正确编译。

interface CalculationOf2Numbers {
    num1: number,
    num2?: number,
    calculate?: () => number
}

let calculator: CalculationOf2Numbers;
calculator = { num1: 1 };
calculator = { num1: 1, num2: 2 };
calculator = {
    num1: 1,
    calculate: function () {
        return this.num1;
    }
};
calculator = {
    num1: 1,
    num2: 2,
    calculate: function () {
        return this.num1 + this.num2;
    }
};

只读修饰符

对象的属性和方法默认情况下是支持读写的。第 6 章提到,const 关键字只能限定栈上的内容不可编辑,而对象是引用类型,其数据存储在堆上,因此 const 关键字只能限定引用的对象地址不变,但无法限定堆上的属性和方法不变。例如,在以下代码中,虽然 calculator 的引用对象不可更改,但其属性值可以随意修改。

interface CalculationOf2Numbers {
    num1: number,
    num2: number,
    calculate: () => number
}

const calculator: CalculationOf2Numbers = { num1: 1, num2: 2, calculate: function ()
{ return this.num1 + this.num2; } };

//以下代码会引起编译错误
//编译错误:无法分配到 "calculator",因为它是常数。ts(2588)
calculator = { num1: 3, num2: 4, calculate: function () { return 1; } }

//以下代码可以正确编译
calculator.num1 = 3;
calculator.num2 = 4;
calculator.calculate = function () { return 1; };

要限定对象的属性和方法不可编辑,就需要在属性声明或方法声明的前面加上 readonly 关键字。例如,在以下代码中,对只读属性或方法进行编辑会引起编译错误。

interface CalculationOf2Numbers {
    readonly num1: number,
    readonly num2: number,
    readonly calculate: () => number
}

const calculator: CalculationOf2Numbers = { num1: 1, num2: 2, calculate: function
() { return this.num1 + this.num2; } };

//编译错误:无法分配到 "num1",因为它是只读属性。ts(2540)
calculator.num1 = 3;
//编译错误:无法分配到 "num2",因为它是只读属性。ts(2540)
calculator.num2 = 4;
//编译错误:无法分配到 "calculate",因为它是只读属性。ts(2540)
calculator.calculate = function () { return 1; };

索引签名

TypeScript 中,如果实际赋值的对象的属性与方法比接口中定义的属性与方法多,那么将该对象的值赋给该接口类型会引起编译错误。例如,在以下代码中,接口中只定义了 nameage 属性,但实际赋值的对象还有 height 属性,这会引起编译错误。

interface Person {
    name: string,
    age: number
}

let person1: Person = { name: "Kiddy", age: 17 };
//编译错误:不能将类型"{ name: string; age: number; height: number; }"分配给类型"Person"。
//对象文字可以只指定已知属性,并且"height"不在类型"Person"中。ts(2322)
let person2: Person = { name: "Shark", age: 15, height: 180 };

要解决这个问题,用前面提到的可选修饰符 ?height 作为可选参数加入接口定义中。但这只适用于已知有哪些可选属性或方法的情况,如果遇到完全不确定有哪些可选属性或方法的情况应该怎么办呢?这时就需要用到索引签名。通过索引签名,让接口支持任意数量的可选属性。索引签名的定义方式如下。

interface 接口名称 {
    ...
    [索引名称:索引类型]:属性类型;
    ...
}

TypeScript 只支持两种类型的索引——字符串索引和数值索引。如果使用字符串索引,则表示这些任意数量的可选属性名称只能是字符串(数字也是字符串);如果使用数值索引,则表示这些任意数量的可选属性名称只能是数字。

  1. 字符串索引

    以下代码定义了一个字符串索引,这意味着属性名称可以是任意字符串,它支持存储 any 类型的值,因此支持任意类型的可选属性和方法。

    interface Person {
        name: string,
        age: number,
        [index: string]: any
    }
    
    let person1: Person = { name: "Kiddy", age: 17 };
    let person2: Person = { name: "Shark", age: 15, height: 180 };
    let person3: Person = { name: "Annie", age: 10, height: 120, sex: "male" };
    let person4: Person = { name: "Aiken", age: 25, height: 174, sayHello: function () { console.log("hello!") } };
  2. 数值索引

    如果定义为数值索引,则属性名称只能由数字组成;否则,会引起编译错误,示例代码如下。

    interface Person {
        name: string,
        age: number,
        [index: number]: any
    }
    
    let person1: Person = { name: "Kiddy", age: 17 };
    let person2: Person = { name: "Shark", age: 15, 1: 180 };
    let person3: Person = { name: "Annie", age: 10, 1: 120, 2: "male" };
    let person4: Person = { name: "Aiken", age: 25, 1: 174, 3: function () { console.log
    ("hello!") } };
    
    //编译错误:不能将类型"{ name: string; age: number; sex: string; }"分配给类型"Person"
    //对象文字可以只指定已知属性,并且"sex"不在类型"Person"中。ts(2322)
    let person5: Person = {name:"Error", age:16, sex:"male"};
  3. 索引与已有属性的关系

    严格来说,索引表示所有属性或方法不仅包含未定义的属性或方法,还包含已定义的所有属性和方法。例如,之前定义的 Person 接口拥有 nameage 两个属性,这意味着如果定义字符串索引,nameage 这两个名称会匹配字符串索引。因此在定义字符串索引时,其属性类型至少需要包含 nameage 的属性类型,否则会引起编译错误。

    以下代码定义了一个属性类型为字符串的字符串索引,但字符串索引除包含未定义的属性之外,还包含 nameage 这两个已定义的属性。如果索引的属性类型定义为 string,就无法包含 agenumber 类型,因此会引起编译错误。

    interface Person {
        name: string,
        //编译错误:类型"number"的属性"age"不能赋给"string"索引类型"string"。ts(2411)
        age: number,
        [index: string]: string
    }

    要解决这个问题,参考前面的示例,将索引的属性类型定义为 any 类型。但 any 类型不仅包含 string 类型和 number 类型,还支持其他类型。如果能确定所有属性仅为 string 类型和 number 类型,则可以将索引的属性类型定义为 string | number(联合类型,可以用 “|” 连接多个类型,后面会详细介绍),这样所有未定义的属性的值既可以为 number 类型也可以为 string 类型,同时这种类型包含已定义的 name 属性和 age 属性的类型,不再引起编译错误,代码如下。

    interface Person {
        name: string,
        age: number,
        [index: string]: string | number
    }
  4. 其他支持索引的类型

除对象之外,其他引用类型也支持带有索引的接口,例如,数组支持数值索引,代码如下。

interface StringArray {
    [index: number]: string
}

let array1: StringArray = ["x", "y", "x"];

以上代码声明了一个 StringArray 接口,它拥有数值索引,属性类型为字符串,该接口和字符串数组匹配,因此可以将数组的值赋给该接口类型的变量。