TypeScript 数据类型

typescript

TypeScript 数据类型

TypeScript 提供了类型系统,除了 JavaScript 提供了的数据类型,还添加了额外的数据类型,如枚举 enum,还支持自定义更复杂的数据类型,类似于数据库的 Scheme。

JavaScript 的类型分为两种:原始数据类型 Primitive data types 和对象类型 Object types

原始数据类型包括:

  • 布尔值
  • 数值
  • 字符串
  • null
  • undefined
  • Symbol(ES6 中的新类型)
  • BigInt(ES10 中的新类型)
Tip

在 TypeScript 文档里,对于变量(或函数的形参)可以在其后使用 : 指定数据类型(以上所述的 JavaScript 内置的类型,冒号的前后有没有空格都可以);对于函数除了可以约束输入(形参),还可以指定输出的数据类型,其中 void 类型就是针对函数无返回值的情况。

布尔值

在 TypeScript 中使用 boolean 定义布尔值类型

ts
let isDone: boolean = false;
Warning

使用构造函数 new Boolean() 创造的对象是布尔值

ts
let createdByNewBoolean: boolean = new Boolean(1);

// Type 'Boolean' is not assignable to type 'boolean'.
// 'boolean' is a primitive, but 'Boolean' is a wrapper object. Prefer using 'boolean' when possible.

事实上 new Boolean() 返回的是一个 Boolean 对象,而直接调用 Boolean 才返回一个 boolean 类型。类似地,其他基本类型(除了 nullundefined)也是一样。

ts
let createdByBoolean: boolean = Boolean(1);

数值

在 TypeScript 中使用 number 定义数值类型

ts
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
// ES6 中的二进制表示法
let binaryLiteral: number = 0b1010;
// ES6 中的八进制表示法
let octalLiteral: number = 0o744;
let notANumber: number = NaN;
let infinityNumber: number = Infinity;
Tip

包括 NaNInfinity 特殊的数值

字符串

在 TypeScript 中使用 string 定义字符串类型

ts
let myName: string = 'Tom';
// 模板字符串
let sentence: string = `Hello, my name is ${myName}.`

字符串字面量

字符串字面量类型用来约束变量的取值只能是某几个字符串中的一个

ts
type EventNames = 'click' | 'scroll' | 'mousemove';
function handleEvent(ele: Element, event: EventNames) {
    // do something
}

handleEvent(document.getElementById('hello'), 'scroll');  // 没问题
handleEvent(document.getElementById('world'), 'dblclick'); // 报错,event 不能为 'dblclick'

// index.ts(7,47): error TS2345: Argument of type '"dblclick"' is not assignable to parameter of type 'EventNames'.

以上示例中使用 type 定了一个字符串字面量类型 EventNames,它只能取三种字符串中的一种。

Tip

类型别名与字符串字面量类型都是使用 type 进行定义,实际上两者的作用都是一样的,都是创建一个自定的类型。只是联合类型是对类型的限制在特定的几个类型中,而字符串字面量是对值的限制在特定的几个字符串中。

空值

JavaScript 没有空值 void 的概念,但在 TypeScript 文档中可以使用 void 表示没有任何返回值的函数

ts
function alertName(): void {
    alert('My name is Tom');
}
Tip

声明一个 void 类型的变量没有什么用,因为你只能将它赋值为 undefinednull,这样的变量不可变且一般没什么用

Null 和 Undefined

在 TypeScript 中使用 nullundefined 来定义这两个原始数据类型

ts
let u: undefined = undefined;
let n: null = null;
Tip

void 的区别是,==undefinednull 是所有类型的子类型==,即例如设定为 undefined 类型的变量,可以赋值给 number 类型的变量;而 void 类型的变量不能赋值给 number 类型的变量。

ts
let u: undefined;
let num: number = u; // 这样也不会报错
ts
let u: void;
let num: number = u;

// Type 'void' is not assignable to type 'number'.

任意值

在 TypeScript 文档中使用 any 来表示变量允许赋值为任意类型,而且变量允许在上下文中被赋值更改时,数据类型为任意类型,因为 any 类型完全不被类型检查

ts
let myFavoriteNumber: any = 'seven';
myFavoriteNumber = 7;
Tip

声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值,因此可以在该变量上访问任何属性或调用任何方法,在 ts 编译为 js 阶段都不会报错。变量如果在声明的时未指定其类型,且未设定初始值,那么它会被识别为任意值类型

ts
let something;
something = 'seven';
something = 7;

对象

可以使用 object 来约束变量的数据类型为对象

ts
let person: object = {
  name: 'Ben';
  age: 666
}

在 TypeScript 中还可以来更详细地定义对象的类型,包括其内部各属性的数据类型

ts
let person: {
  name: string,
  age: number
}

person = {
  name: 'Tom',
  age: 333
}

接口

为了复用对象的约束,TypeScript 提供==关键字接口 Interfaces 来声明对象的数据类型,类似于数据库的 scheme,用于描述对象的「形状」 Shape==,它可以(像类一样)通过继承来构建复杂的对象类型。接口的名称一般首字母大写。

ts
interface Person {
    name: string;
    age: number;
    // 对象的方法所对应的数据结构
    speak(c: string): string; // 推荐这种写法,和 ES6 中定义对象的方法的形式相一致
    spend: (n: number) => void;
}

let tom: Person = {
    name: 'Tom',
    age: 25,
    // 对象的方法
    speak: (content:string) => content,
    spend(num: number) {
        console.log(num)
    }
};

以上的示例中定义了一个接口 Person,接着定义了一个变量 tom,它的类型是 Person,这样就约束了 tom 的「形状」必须和接口 Person 一致。

Tip

在面向对象语言中,接口 Interfaces 是对行为的抽象,而具体如何行动需要由类 classes 去实现 implement。TypeScript 的接口 interfaces 还用于对的一部分行为进行抽象。有的编程语言中会建议接口的名称加上 I 前缀

Warning

如果变量赋值时,所指向的对象属性必须和接口的形状保持一致,比接口定义的属性少了一些或多了一些默认是不允许的,会造成编译报错。

ts
interface Person {
    name: string;
    age: number;
}

let tom: Person = {
    name: 'Tom'
};

// index.ts(6,5): error TS2322: Type '{ name: string; }' is not assignable to type 'Person'.
//   Property 'age' is missing in type '{ name: string; }'.

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

// index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.

可选属性

如果希望变量不要完全匹配一个接口的形状,那么可以在定义接口时使用可选属性,在属性名后? 标注,即该属性在对象中可以不存在(但赋值给变量的对象仍然不允许添加接口未定义的属性

ts
interface Person {
    name: string;
    age?: number;
}

let tom: Person = {
    name: 'Tom'
};

任意属性

如果希望一个接口更灵活有更大的可拓展性,可以在定义接口时添加任意属性,它的属性名可以「预留」着,之确定其数据类型

Warning

一个接口中只能定义一个任意属性

ts
interface Person {
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    name: 'Tom',
    gender: 'male'
};

以上示例中使用 [propName: string] 定义了一个任意属性,它的属性名是 String 类型,属性值是任意类型

Warning

一旦定义了任意属性,那么==接口中其他的确定属性可选属性的数据类型都必须是该任意属性的数据类型的子集==。因此如果接口中有多种不同数据类型的属性,那么在定义任意属性时,它的属性值的数据类型应该为 any 或使用联合类型

ts
interface Person {
    name: string;
    age?: number;
    [propName: string]: string;
}

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

// index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
// index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Index signatures are incompatible.
//     Type 'string | number' is not assignable to type 'string'.
//       Type 'number' is not assignable to type 'string'.

以上示例中任意属性的值允许是 string,但是可选属性 age 的值却是 numbernumber 不是 string 的子集,所以报错了。

ts
interface Person {
    name: string;
    age?: number;
    [propName: string]: string | number;
}

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

只读属性

如果希望对象中的一些属性只能在创建对象的时候被赋值,可以在定义接口时用 readonly 限制属性为只读属性

ts
interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    id: 89757,
    name: 'Tom',
    gender: 'male'
};

tom.id = 9527;

// index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

以上示例中使用 readonly 定义的属性 id 初始化后,又被赋值了,所以报错了。

Tip

只读属性的约束是指该属性只能够在第一次给变量赋值的时候设置其值(之后就不能重新赋值修改了),而不是第一次给只读属性赋值的时候

ts
interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    name: 'Tom',
    gender: 'male'
};

tom.id = 89757;

// index.ts(8,5): error TS2322: Type '{ name: string; gender: string; }' is not assignable to type 'Person'.
//   Property 'id' is missing in type '{ name: string; gender: string; }'.
// index.ts(13,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

以上示例报错信息有两处,第一处是在对 tom 进行赋值的时候,没有给只读属性 id 赋值;第二处是在给 tom.id 赋值的时候,由于它是只读属性不能再赋值,所以报错了。

数组

在 TypeScript 中,数组类型有多种定义方式。

Tip

一般而言,数组 Array 是指用于存储数据类型相同的一系列元素的一种数据结构,虽然可以使用 any[] 来支持格元素是任意数据类型,但不推荐;而元组 Tuple 则是用于存储数据类型不同的一系列元素的一种数据结构,而且依次明确指定了各个元素的数据类型。

类型+方括号

最简单的方法是使用 typeName[] 来定义数组类型及其中的元素的类型

ts
let fibonacci: number[] = [1, 1, 2, 3, 5];

以上示例定义变量 fibonacci 的数据类型是数组,而且数组各项的数据类型是 number

Tip

数组的一些方法的参数也会根据数组在定义时约定的类型进行限制

ts
let fibonacci: number[] = [1, 1, 2, 3, 5];
fibonacci.push('8');

// Argument of type '"8"' is not assignable to parameter of type 'number'.

以上示例中 push 方法只允许传入符合 number 类型的参数,但是却传了一个 "8" 字符串类型的参数,所以报错了。

Tip

如果希望数组中各元素的数据类型可以是任意的,一个常见的做法是使用任意值 any

ts
let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];

数组泛型

使用数组泛型 Array Generic Array<elemType> 来表示数组

ts
let fibonacci: Array<number> = [1, 1, 2, 3, 5];

接口

接口也可以用来描述数组,这种方式更复杂(接口需要以对象的方式定义,需要指定索引的类型是数值,并且指定元素的数据类型),一般用于定义类数组

ts
interface NumberArray {
    [index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

类数组

在 TypeScript 中能用普通的 typeName[] 方式来定义类数组,而需要使用接口的方式来定义。因为类数组 Array-like Object 不是数组类型,比如 arguments(函数参数对象)就是一个类数组对象。

ts
function sum() {
    // 定义一个变量 args 来「接收」 arguments 类数组对象(函数的所有入参)
    let args: {
        [index: number]: number;
        length: number;
        callee: Function;
    } = arguments;
}

以上示例使用接口的方式定义了 arg 变量的类型(相当于定义了 arguments 类数组对象的数据类型),其中约束索引的类型是数字,值的类型必须是数字;也约束了 lengthcallee 两个属性。

Tip

事实上 TypeScript 为常用的类数组对象内置了接口定义,如 IArguments, NodeList, HTMLCollection 等,因此以上的定义可以简化为

ts
function sum() {
    let args: IArguments = arguments;
}

其中 IArguments 是 TypeScript 中定义好了的类型,它实际上就是

ts
interface IArguments {
    [index: number]: any;
    length: number;
    callee: Function;
}

元组

元组 Tuple 是一种与数组类似的数据类型,但它各项元素可以是不同数据类型,使用 [type1, type2] 约束元组中各项的数据类型,而且这些数据类型是有序的。

如果直接对元组类型的变量进行赋值(初始化)的时候,需要提供所有元组类型中指定的项,否则编译时会报错;而如果使用索引,则可以只赋值其中一项

ts
let tom: [string, number] = ['Tom', 25];
// 报错,由于元素的数据类型不能依次对应
// let tom: [string, number] = [25, 'Tom'];
ts
let tom: [string, number];
tom = ['Tom'];

// Property '1' is missing in type '[string]' but required in type '[string, number]'.
ts
// 只赋值其中一项
let tom: [string, number];
tom[0] = 'Tom';
Tip

允许添加额外的元素,但是「越界」元素的类型会被限制为元组中其他各项类型的联合类型

ts
let tom: [string, number];
tom = ['Tom', 25];
tom.push('male');
tom.push(true);

// Argument of type 'true' is not assignable to parameter of type 'string | number'.

枚举

枚举类型 Enum 用于限定取值在一定范围内的场景,使用 enum 关键字来定义该类型。

枚举成员会默认被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射

相当于为各个枚举值 mapping 一个索引,可以把枚举变量看作是一个对象,然后通过 enumName.key 来获取该枚举值 key 所 mapping 的索引。

这里的好处是为数值(索引)赋予了含义,例如当使用一串数字表示状态时,可以通过枚举来赋予它们不同的「名称」,在开发是通过 enumName.key 来使用这些数字,不仅含义明确便于后期维护,而且还有相应的代码提示。

ts
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};

Days.Sun; // 0

Days["Sun"]; // 0

// console.log(Days["Sun"] === 0); // true
// console.log(Days["Mon"] === 1); // true
// console.log(Days["Tue"] === 2); // true
// console.log(Days["Sat"] === 6); // true

// console.log(Days[0] === "Sun"); // true
// console.log(Days[1] === "Mon"); // true
// console.log(Days[2] === "Tue"); // true
// console.log(Days[6] === "Sat"); // true

🔨 编译结果

js
var Days;
(function (Days) {
    Days[Days["Sun"] = 0] = "Sun";
    Days[Days["Mon"] = 1] = "Mon";
    Days[Days["Tue"] = 2] = "Tue";
    Days[Days["Wed"] = 3] = "Wed";
    Days[Days["Thu"] = 4] = "Thu";
    Days[Days["Fri"] = 5] = "Fri";
    Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));
Tip

可以给枚举项手动赋值,而未手动赋值的枚举项会接着上一个枚举项递增。

ts
enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat};

console.log(Days["Sun"] === 7); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true

但是如果未手动赋值的枚举项与手动赋值的重复了,TypeScript 是不会察觉到这一点,因此编译时不会报错,但会导致覆盖情况

ts
enum Days {Sun = 3, Mon = 1, Tue, Wed, Thu, Fri, Sat};

// console.log(Days["Sun"] === 3); // true
// console.log(Days["Wed"] === 3); // true
// console.log(Days[3] === "Sun"); // false
// console.log(Days[3] === "Wed"); // true

🔨 编译结果

js
var Days;
(function (Days) {
    Days[Days["Sun"] = 3] = "Sun";
    Days[Days["Mon"] = 1] = "Mon";
    Days[Days["Tue"] = 2] = "Tue";
    Days[Days["Wed"] = 3] = "Wed"; // 覆盖 3 的指向
    Days[Days["Thu"] = 4] = "Thu";
    Days[Days["Fri"] = 5] = "Fri";
    Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));

常数项和计算所得项

枚举的项有两种类型:

  • 常数项 constant member
    之前示例中都是常数项,遵循一定的规则:
    • 不具有初始化函数并且之前的枚举成员是常数。第一个枚举元素初始值默认为 0,后一个枚举成员的值是上一个枚举成员的值加 1
    • 枚举成员使用常数枚举表达式初始化。常数枚举表达式是 TypeScript 表达式的子集,它可以在编译阶段求值。
    ts
    enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
    
  • 计算所得项 computed member
    ts
    enum Color {Red, Green, Blue = "blue".length};
    

    💡 如果紧接在计算所得项后面的是未手动赋值的项,那么它就会因为无法获得初始值而报错
    ts
    enum Color {Red = "red".length, Green, Blue};
    
    // index.ts(1,33): error TS1061: Enum member must have initializer.
    // index.ts(1,40): error TS1061: Enum member must have initializer.
    

常数枚举

常数枚举是使用 const enum 定义的枚举类型,与普通枚举的区别是,它会在编译阶段被删除,并且成员能包含计算所得项。

ts
const enum Directions {
    Up,
    Down,
    Left,
    Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

🔨 编译结果

js
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];

外部枚举

外部枚举 Ambient Enums 是使用 declare enum 定义的枚举类型,外部枚举与声明语句一样,常出现在声明文件中。

ts
declare enum Directions {
    Up,
    Down,
    Left,
    Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

🔨 编译结果

ts
var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

函数

在 TypeScript 中可以使用 Function(注意大写开头)约束变量为函数

ts
let greet: Function;

greet = () => {
  console.log('hello')
}

还可以对函数进行更详细的约束,包括它的输入参数的数据类型和函数返回输出的数据类型:

  • 输入的约束在入参中设置
  • 输出的约束在函数名(参数)后设置
Tip

函数的返回值的数据类型可以不进行显式声明,因为 TypeScript 会根据函数的返回值自动进行类型推论

在 JavaScript 中函数有两种常见的函数定义方式:

  • 函数声明 Function Declaration
  • 函数表达式 Function Expression(匿名函数)
ts
// 函数声明
function sum(x: number, y: number): number {
    return x + y;
}

// 函数表达式
let mySum = function (x: number, y: number): number {
    return x + y;
};
// 上面的代码只对等号右侧的匿名函数进行了类型定义
// 而等号左边的 mySum,是通过赋值操作进行类型推论而推断出来的
// 可以手动给 mySum 添加函数类型,使用箭头 => 约束输入和输出
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
    return x + y;
};

函数签名

以上使用了函数签名 Function Signature 对函数表达式的变量进行约束,函数签名定义了函数或方法的输入与输出。不要混淆了 TypeScript 中的 => 和 ES6 中的 =>。在 TypeScript 的类型定义中 => 用来表示函数的定义,左边是输入类型,需要用括号 () 括起来;右边是输出类型。

ts
// 使用函数签名约束变量 greet 所指向的函数的结构类型
let greet: (a: string, b: string) => void;

// 变量具体指向的函数,其形参变量名称可以不同,但是数据类型需要与约束一致;函数的返回值也需要与约束一致
greet = (name: string, greeting: string) => {
  console.log(`${name} says ${greeting}`)
}
Warning

定义函数时,输入多余的(或者少于要求的)参数是不被允许的,否则编译时会报错

ts
function sum(x: number, y: number): number {
    return x + y;
}
sum(1, 2, 3);

// index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.
Tip

有时候函数需要根据输入的数据类型,决定输出的数据类型,在 TypeScript 中可以使用重载为同一个函数定义多套输入和输出相匹配的类型约束

ts
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string | void {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

以上示例实现一个函数 reverse,当输入数字时,反向输出的也是数字;当输入字符串时,反向输出的也是字符串。我们对函数 reverse 重复定义了多次,前两次都是函数定义,最后一次是函数实现,由于 TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面,然后在编辑器的调用该函数时,会在代码提示中看到前两个提示中的一个。

可选参数

如果希望入参是可选的,可以在定义时使用可选参数,在形参后用 ? 标注,而且可选参数必须接在必需参数后面,即可选参数后面不允许再出现必需参数了

ts
function buildName(firstName: string, lastName?: string) {
    if (lastName) {
        return firstName + ' ' + lastName;
    } else {
        return firstName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
Tip

在 ES6 中允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数(如果该参数同时也真的是可选参数,它也不需要在形参后添加 ?),但这种情况下,受「可选参数必须接在必需参数后面」的限制

ts
function buildName(firstName: string = 'Tom', lastName: string) {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');

剩余参数

在 ES6 中可以在定义函数时,使用 ...rest 作为最后的形参,获取调用函数时传入的剩余参数,由于剩余参数是将不定数量的实参绑定到一个数组,其中 rest 就是该数组的名称,因此在 TypeScript 中可以使用数组类型来定义它

ts
function push(array: any[], ...items: any[]) {
    items.forEach(function(item) {
        array.push(item);
    });
}

let a = [];
push(a, 1, 2, 3);

接口

对于函数表达式还可以使用接口的方式来约束。接口对象的属性(左侧)需要用括号 () 括起来,表示是函数的输入类型;属性值(右侧)是输出类型

ts
interface SearchFunc {
    (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    return source.search(subString) !== -1;
}

类是对象的「蓝图」,通过实例化类可以得到一个对象,这些对象都是具有类似属性和方法的。

在 ES6 中使用关键字 class 来定义类,使用关键字 new 来实例化以获得对象,而 TypeScript 除了实现了所有 ES6 中的类的功能以外,还添加了一些新语法(访问修饰符)。

为了更清晰,可以在类的构造函数前先声明属性/变量的数据类型约束

ts
class Animal {
  // 可以先定义类的属性 name 的数据类型
  name: string;
  // 入参和属性名可以不同
  constructor(n: string) {
    this.name = n;
  }
  // 定义方法返回的数据类型
  sayHi(): string {
    return `My name is ${this.name}`;
  }
}

let a: Animal = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack
Tip

也可以进行整合,在构造函数的形参中进行属性的数据类型的声明,然后 TypeScript 编译器会自动为我们在类的构造函数内部,将这些入参赋值给同名的属性,但是这些形参需要显式标注出相应的访问修饰符

ts
class Animal {
  constructor(public name: string) {}
  // 定义方法返回的数据类型
  sayHi(): string {
    return `My name is ${this.name}`;
  }
}

let a: Animal = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack

访问修饰符

TypeScript 使用三种访问修饰符 Access Modifiers 限制对类内部的属性或方法的访问权限:

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public
  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的
ts
/*
 * name 设置为 public
 */
// 直接访问实例的 name 属性是允许的
class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
}

let a = new Animal('Jack');
console.log(a.name); // Jack

/*
 * name 设置为 private
 */
// 只能在类的定义中进行访问
// 无法在实例中直接存取 name
class Animal {
  private name;
  public constructor(name) {
    this.name = name;
  }
}

let a = new Animal('Jack');
console.log(a.name);
a.name = 'Tom';

// index.ts(9,13): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
// index.ts(10,1): error TS2341: Property 'name' is private and only accessible within class 'Animal'.

// 在子类 Cat 中也无法访问父类的 name 属性
class Cat extends Animal {
  constructor(name) {
    super(name);
    console.log(this.name);
  }
}

// index.ts(11,17): error TS2341: Property 'name' is private and only accessible within class 'Animal'.

/*
 * name 设置为 protect
 */
// 允许在类的定义中和子类中访问
class Animal {
  protected name;
  public constructor(name) {
    this.name = name;
  }
}

class Cat extends Animal {
  constructor(name) {
    super(name);
    console.log(this.name);
  }
}
Tip

修饰符还可以直接应用中构造函数的参数中,使代码更简洁,TypeScript 会在类的构造函数内部定义出同名的属性,并将相应的参数赋值给该属性

ts
class Animal {
  // public name: string;
  constructor(public name) {
    // this.name = name;
  }
}
Tip

TypeScript 访问修饰符的限制只是作用在编译阶段,因为在编译之后的 JS 代码中,对于设置了 private 的属性,并没有语法支持去限制其外部的可访问性

ts
/*
 * name 设置为 private
 */
// 只能在类的定义中进行访问
// 无法在实例中直接存取 name
class Animal {
  private name;
  public constructor(name) {
    this.name = name;
  }
}

let a = new Animal('Jack');
console.log(a.name);
a.name = 'Tom';

// index.ts(9,13): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
// index.ts(10,1): error TS2341: Property 'name' is private and only accessible within class 'Animal'.

🔨 编译结果

js
var Animal = (function () {
  function Animal(name) {
    this.name = name;
  }
  return Animal;
})();
var a = new Animal('Jack');
console.log(a.name);
a.name = 'Tom';
Warning

构造函数修饰为 private 时,该类不允许被继承或者实例化;而当构造函数修饰为 protected 时,该类只允许被继承

ts
/*
 * 构造函数修饰为 private
 */
class Animal {
  public name;
  private constructor(name) {
    this.name = name;
  }
}

class Cat extends Animal {
  constructor(name) {
    super(name);
  }
}

let a = new Animal('Jack');

// index.ts(7,19): TS2675: Cannot extend a class 'Animal'. Class constructor is marked as private.
// index.ts(13,9): TS2673: Constructor of class 'Animal' is private and only accessible within the class declaration.

/*
 * 构造函数修饰为 protected
 */
class Animal {
  public name;
  protected constructor(name) {
    this.name = name;
  }
}
class Cat extends Animal {
  constructor(name) {
    super(name);
  }
}

let a = new Animal('Jack');

// index.ts(13,9): TS2674: Constructor of class 'Animal' is protected and only accessible within the class declaration.

readonly

只读属性通过关键字 readonly 进行设置,它会限制属性赋予初始值后无法再进行修改

ts
class Animal {
  readonly name;
  public constructor(name) {
    this.name = name;
  }
}

let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';

// index.ts(10,3): TS2540: Cannot assign to 'name' because it is a read-only property.
Tip

如果将其使用在构造函数参数中,与其他访问修饰符同时存在的话,需要写在其后面。

ts
class Animal {
  // public readonly name;
  public constructor(public readonly name) {
    // this.name = name;
  }
}

abstract

关键字 abastract 用于定义抽象类和抽象方法(类似于占位符,没有写实际的实现逻辑代码)。抽象类是允许被实例化的,只能用于被继承。其中抽象方法必须被子类实现。

ts
abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}

let a = new Animal('Jack');

// index.ts(9,11): error TS2511: Cannot create an instance of the abstract class 'Animal'.
ts
abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}


// 类 Cat 继承了抽象类 Animal
// 但是没有实现抽象方法 sayHi,所以编译报错了
class Cat extends Animal {
  public eat() {
    console.log(`${this.name} is eating.`);
  }
}

let cat = new Cat('Tom');

// index.ts(9,7): error TS2515: Non-abstract class 'Cat' does not implement inherited abstract member 'sayHi' from class 'Animal'.
ts
abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}

// 类 Cat 继承了抽象类 Animal,并且在其中实现抽象方法 sayHi
class Cat extends Animal {
  public sayHi() {
    console.log(`Meow, My name is ${this.name}`);
  }
}

let cat = new Cat('Tom');

接口

接口 Interfaces 除了可用于描述对象的「形状」shape,还可以对类的一部分行为/方法进行抽象。有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口 interfaces,然后在需要该功能的类中通过 implements 关键字来实现。通过这个特性可以复用一些类的约束条件。

ts
interface Alarm {
  alert(): void;
}

// 父类
class Door {
}

// 子类
// 子类「防盗门」拓展自父类「门」,还有实现了「报警」功能
class SecurityDoor extends Door implements Alarm {
  alert() {
    console.log('SecurityDoor alert');
  }
}

// 另一个类「车」也实现了「报警」功能
class Car implements Alarm {
    alert() {
        console.log('Car alert');
    }
}

以上示例中将报警方法提取成为接口 interface,然后在需要使用报警功能的类上实现 implements 该功能,例如防盗门和车。

Tip

一个子类只能继承自一个父类,但是约束其「形状」的接口可以有多个

ts
interface Alarm {
    alert(): void;
}

interface Light {
    lightOn(): void;
    lightOff(): void;
}

// 一个类可以实现多个接口
class Car implements Alarm, Light {
    alert() {
        console.log('Car alert');
    }
    lightOn() {
        console.log('Car light on');
    }
    lightOff() {
        console.log('Car light off');
    }
}

接口继承接口

类似于类的继承,接口也可以继承接口

ts
interface Alarm {
    alert(): void;
}

interface LightableAlarm extends Alarm {
    lightOn(): void;
    lightOff(): void;
}

以上示例表示 LightableAlarm 继承了 Alarm,除了拥有 alert 方法之外,其描述的「形状」中还拥有两个新方法 lightOnlightOff

接口继承类

在 TypeScript 中接口还可以继承自类。

因为实际上当我们在声明一个类时,除了会创建一个类之外,同时也创建了一个同名的的类型(实例的类型)。

ts
class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

等价于

ts
class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

// 手动创建一个接口
// 与声明 class Point 时创建的 Point 类型是等价的
interface PointInstanceType {
    x: number;
    y: number;
}

interface Point3d extends PointInstanceType {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

因此它既可以当类来使用(通过关键字 new 进行实例化),也可以当作类型使用(例如通过 var : className 来约束变量的数据类型),即类包括具体的实现逻辑代码,同时也包括数据类型的「形状」信息。

ts
// 将 Point 当做一个类来用
class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

const p = new Point(1, 2);
ts
// 将 Point 当做一个类型来用
class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

function printPoint(p: Point) {
    console.log(p.x, p.y);
}

printPoint(new Point(1, 2));
Tip

值得注意的是声明类的同时所创建的相应类型是包含构造函数和静态属性或静态方法的,换句话说声明类时所创建的相应类型只包含其中的实例属性实例方法

ts
class Point {
    /** 静态属性,坐标系原点 */
    static origin = new Point(0, 0);
    /** 静态方法,计算与原点距离 */
    static distanceToOrigin(p: Point) {
        return Math.sqrt(p.x * p.x + p.y * p.y);
    }
    /** 实例属性,x 轴的值 */
    x: number;
    /** 实例属性,y 轴的值 */
    y: number;
    /** 构造函数 */
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
    /** 实例方法,打印此点 */
    printPoint() {
        console.log(this.x, this.y);
    }
}

interface PointInstanceType {
    x: number;
    y: number;
    printPoint(): void;
}

// 类型 Point 和类型 PointInstanceType 是等价的
let p1: Point;
let p2: PointInstanceType;

联合类型

在 TypeScript 中支持将变量的数据类型设定为联合类型 Union Types,表示取值可以是多种类型中的一种,每个数据类型之间使用 | 分隔

ts
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

let arr: (string | number | boolean)[] = [];
arr.push('test');
arr.push(123);
arr.push(true);

以上示例中 let myFavoriteNumber: string | number 的含义是,允许 myFavoriteNumber 的类型是 string 或者 number,但是能是其他类型。对于变量 arr 约束为数组,而数组的元素可以是 stringnumberboolean 三者之一。

Warning

在编译时,如果 TypeScript 不确定一个联合类型的变量到底是哪个类型(一般是在针对函数形参的情况),该变量只能访问这些类型里共有的属性或方法,否则编译会报错

ts
function getLength(something: string | number): number {
    return something.length;
}

// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
//   Property 'length' does not exist on type 'number'.

function getString(something: string | number): string {
    return something.toString();
}

以上示例中 length 不是 stringnumber 的共有属性,所以会报错;而访问 stringnumber 的共有属性 toString 是没问题的。

而联合类型的变量一般在被赋值的时候,会根据类型推论的规则推断出一个类型

ts
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // 编译时报错

// index.ts(5,30): error TS2339: Property 'length' does not exist on type 'number'.

以上示例中当 myFavoriteNumber 被赋值为字符串,被推断成了 string,访问它的 length 属性不会报错;而 myFavoriteNumber 被赋值为数值时,被推断成了 number,访问它的 length 属性时就报错了。

类型别名

使用 type 来给一个类型创建别名,便于之后重复使用,常用于联合类型。

ts
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    } else {
        return n();
    }
}

泛型

泛型 Generics 是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

类似于占位符,一般使用 <T> 表示,其中 T stand for type,可以是其他任意值,表示类型。很适合输出的数据类型基于输入的数据类型这种情况。

ts
function createArray(length: number, value: any): Array<any> {
    let result = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

// 使用泛型,基于输入的数据类型约束输出的数据类型
function createArray<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray<string>(3, 'x'); // ['x', 'x', 'x']

以上示例创建了一个函数 createArray,它的作用是创建一个指定长度的数组。

在函数名后添加了 <T>,其中 T (也可以用其他符号表示,但是约定俗成用 T)用来指代任意输入的类型,然后在后面就可以使用了。在输入参数中 value: T 和输出 Array<T> 中,还有函数内的变量 result: T[]

接着在调用函数时,在函数名称后指定它具体的类型为 <string>,这样就可以对相应的参数和变量进行具体的数据类型约束

Tip

也可以不手动指定,而让类型推论自动推算出来,对于相应的参数和变量也可以进行具体的数据类型约束。

ts
createArray(3, 'x'); // ['x', 'x', 'x']
Tip

可以为泛型中的类型参数指定默认类型,当使用泛型时没有显式指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。

ts
function createArray<T = string>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}
Tip

泛型可以继承 extend 其他类型,相当于对泛型进行一个预先的数据格式约束

ts
// 对泛型进行约束,它必须是一个具有 name 属性的对象,但还可以有其他对象(数据结构的约束是继承关系)
const addUID = <T extends {name: string}>(obj: T) => {
    let uid = Math.floor(Math.random() * 100);
    return {...obj, uid}
}

let someObj = addUID(
  {
    name: 'Ben',
    age: 666
  }
);

多个类型参数

定义泛型的时候,可以一次定义多个类型参数

ts
function swap<T, U>(tuple: [T, U]): [U, T] {
    return [tuple[1], tuple[0]];
}

swap([7, 'seven']); // ['seven', 7]

以上示例函数 swap 实现了交换输入的元组的元素的功能

泛型约束

在函数声明的内部,使用泛型定义数据类型的变量,由于事先不知道它是哪种类型,所以能随意的操作它的属性或方法,否则会报错

ts
function loggingIdentity<T>(arg: T): T {
    // 泛型 T 不一定包含属性 length,所以编译的时候报错了
    console.log(arg.length);
    return arg;
}

// index.ts(2,19): error TS2339: Property 'length' does not exist on type 'T'.

可以对泛型进行约束,限制入参的数据符合一定的约束,这样就允许在函数中的变量可以访问特定的属性

ts
interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}

以上示例使用了 extends 约束了泛型 T 必须符合接口 Lengthwise 的「形状」,即入参的数据类型必须包含 length 属性,否则在编译阶段会报错。

Tip

多个类型参数之间也可以互相约束

ts
function copyFields<T extends U, U>(target: T, source: U): T {
    for (let id in source) {
        target[id] = (<T>source)[id];
    }
    return target;
}

let x = { a: 1, b: 2, c: 3, d: 4 };

copyFields(x, { b: 10, d: 20 });

泛型接口

之前提到接口 interface 是用以描述对象、函数或类一部分「形状」的,其实接口可以使用泛型,以便接口所描述的数据类型「形状」更通用

可以用于函数的数据类型的定义

ts
interface CreateArrayFunc {
    <T>(length: number, value: T): Array<T>;
}

// 使用接口约束变量,该变量指向相应的函数
let createArray: CreateArrayFunc;
createArray = function<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']
Tip

可以把泛型参数提前到接口名上,但在后面使用接口时,就需要指定接口中泛型的具体数据类型

ts
interface CreateArrayFunc<T> {
    (length: number, value: T): Array<T>;
}

// 使用接口约束变量,指向相应的函数
// 指定泛型为 any 任意值类型
let createArray: CreateArrayFunc<any>;
createArray = function<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

泛型类

泛型可以用于类的数据类型的定义,使得类所描述的数据类型「形状」更通用

ts
class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

Copyright © 2024 Ben

Theme BlogiNote

Icons from Icônes