类型分类

聊聊TS中的类型分类吧

原始类型

JavaScript 中以下类型被视为原始类型:stringbooleannumberbigintsymbolnullundefined

1
2
3
4
5
6
7
8
9
10
// 注意
// 非严格模式下 null 和 undefined 是所有类型的子类型,就是说你可以把 null 和 undefined 赋值给其他类型。
// 虽然number和bigint都表示数字,但是这两个类型不兼容
let str: string = "SakuraSnow";
let num: number = 16;
let bool: boolean = true;
let u: undefined = undefined;
let n: null = null;
let big: bigint = 100n;
let sym: symbol = Symbol("snow");

对象类型

对象

1
2
3
4
5
6
7
8
9
10
11
// 定义一个type
type User = {
name: string,
age: number
}

// 使用这个type
let user: User = {
name: "Sakura",
age: 16
}

注意,objectObject空对象{}是不同的

  • object代表的是==所有非原始类型==,也就是说我们不能把 numberstringbooleansymbol等 原始类型赋值给 object。在严格模式下,nullundefined 类型也不能赋给 object
  • Object 代表所有拥有 ==toString,hasOwnProperty 方法的类型==,所以所有原始类型、非原始类型都可以赋给 Object。同样,在严格模式下,null undefined类型也不能赋给Object
  • {}空对象类型和大 Object 一样,也是表示原始类型和非原始类型的集合,并且在严格模式下,null undefined 也不能赋给{}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 小object
let lowerCaseObject: object;
lowerCaseObject = 1; // ts(2322)
lowerCaseObject = 'a'; // ts(2322)
lowerCaseObject = true; // ts(2322)
lowerCaseObject = null; // ts(2322)
lowerCaseObject = undefined; // ts(2322)
lowerCaseObject = {}; // ok

// 大object
let upperCaseObject: Object;
upperCaseObject = 1; // ok
upperCaseObject = 'a'; // ok
upperCaseObject = true; // ok
upperCaseObject = null; // ts(2322)
upperCaseObject = undefined; // ts(2322)
upperCaseObject = {}; // ok

// 空对象
let ObjectLiteral: {};
ObjectLiteral = 1; // ok
ObjectLiteral = 'a'; // ok
ObjectLiteral = true; // ok
ObjectLiteral = null; // ts(2322)
ObjectLiteral = undefined; // ts(2322)
ObjectLiteral = {}; // ok。

综上结论:{}、大 Object 是比小 object 更宽泛的类型(least specific),{} 和大 Object 可以互相代替,用来表示原始类型(null、undefined 除外)和非原始类型;而小 object 则表示非原始类型。

从上面示例可以看到,大 Object 包含原始类型,小 object 仅包含非原始类型,所以大 Object 似乎是小 object 的父类型。实际上,大 Object 不仅是小 object 的父类型,同时也是小 object 的子类型。

1
2
3
4
type isLowerCaseObjectExtendsUpperCaseObject = object extends Object ? true : false; // true
type isUpperCaseObjectExtendsLowerCaseObject = Object extends object ? true : false; // true
upperCaseObject = lowerCaseObject; // ok
lowerCaseObject = upperCaseObject; // ok

为什么这么设置呢,我也在想,等我想到再补充

数组

1
2
3
4
// 两种定义方式
let arr: string[] = ["1","2"];
let arr: Array<string> = ["1","2"];
let arr: (number | string)[] = ["1", 2];

函数

函数定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 函数声明
function sum(x: number, y: number): number {
return x + y;
}

// 函数表达式
let sum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};

// 使用接口定义函数
interface SumFunc {
(x: number, y: number): number;
}
函数参数
1
2
3
4
5
6
7
8
// 可选参数
// 注意:可选参数后面不允许再出现必需参数
function buildName(firstName: string, lastName?: string) {
return lastName ? `${firstName} ${lastName}` : firstName;
}

buildName('Sakura', 'Snow');
buildName('Sakura');
1
2
3
4
// 参数默认值
function buildName(firstName: string, lastName: string = "Snow") {
return `${firstName} ${lastName}`;
}
1
2
3
4
5
6
7
8
// 剩余参数
function push<T>(array: Array<T>, ...items: Array<T>): Array<T> {
items.forEach((item: T) => array.push(item));
return array;
}

let a: Array<number> = [];
push<number>(a, 1, 2, 3);
函数重载

函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 提供函数类型的定义
function add(x: number, y: number): number;
function add(x: string, y: string): string;

// 实现函数
function add(x, y) {
return x + y;
}

// 不报错
let num: number = add(1,2);
let str: string = add("Sakura", "Snow");
// 报错
let count: number = add("Sakura", "Snow");

元组

众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在 JavaScript 中是没有元组的,元组是 TypeScript 中特有的类型,其工作方式类似于数组。

元组最重要的特性是可以限制==数组元素的个数和类型==,它特别适合用来实现多值返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 普通元组
type Info = [string, number];

let info: Info = ["Sakura", 16]; // ok
let info: Info = ["Sakura"]; // Error

// 带可选参数的元组
type Tuple = [string, number?];
let tuple: Tuple = ["Sakura", 16]; // ok
let tuple: Tuple = ["Sakura"]; // ok
let tuple: Tuple = [16]; // Error

type Point = [number, number?, number?];
const x: Point = [10]; // 一维坐标点
const xy: Point = [10, 20]; // 二维坐标点
const xyz: Point = [10, 20, 10]; // 三维坐标点

// 带剩余元素的元组
// 元组类型里最后一个元素可以是剩余元素,形式为 ...X,这里 X 是数组类型。剩余元素代表元组类型是开放的,可以有零个或多个额外的元素。 例如,[number, ...string[]] 表示带有一个 number 元素和任意数量string 类型元素的元组类型。
type RestTupleType = [number, ...string[]];
let restTuple: RestTupleType = [666, "Semlinker", "Kakuqo", "Lolo"];

其他类型

void

void表示没有任何类型,和其他类型是平等关系,不能直接赋值

1
2
let a: void; 
let b: number = a; // Error

你只能为它赋予nullundefined(在strictNullChecks未指定为true时)。声明一个void类型的变量没有什么大用,我们一般也只有在函数没有返回值时去声明。

1
2
3
4
// 值得注意的是,方法没有返回值将得到undefined,但是我们需要定义成void类型,而不是undefined类型。
function fun(): void {
console.log("this is TypeScript");
}

never

相比于void表示没有,never表示无法到达

never一般用在下面三种情况

  • 永远抛出错误的函数
  • 死循环的函数
  • 无法到达的分支
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Function returning never must not have a reachable end point
function error(message: string): never {
throw new Error(message);
}

// Inferred return type is never
function fail() {
return error("Something failed");
}

// Function returning never must not have a reachable end point
function infiniteLoop(): never {
while (true) {}
}

// 可以利用 never 类型的特性来实现全面性检查
function foo(data : number|string) {
if (typeof data === "number") {
// ...
} else if (typeof data === "string") {
// ...
} else {
let rua = data;
// Property 'name' does not exist on type 'never'.
console.log(rua.name)
}
}

never类型同nullundefined一样,也是任何类型的子类型,也可以赋值给任何类型。

但是没有类型是never的子类型或可以赋值给never类型(除了never本身之外),即使any也不可以赋值给never

any

表示一个变量的值可以是任何类型,并且去掉类型检查。基本等价于变回js

在许多场景下,这太宽松了。使用 any 类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any 类型,就无法使用 TypeScript 提供的大量的保护机制。请记住,any 是魔鬼!尽量不要用any。

1
2
3
4
5
6
function foo() : any {
return null;
}
// ts不会报错
// 只有运行时才会报错
console.log(foo().data);

unknown

表示一个变量的类型是未知的,每次使用前都要进行类型检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo(): string | number {
if (Math.random() < 0.5) {
return 0;
} else {
return ""
}
}

let data: unknown = foo();
let num = 10;
// 如果不缩小类型,就无法对unknown类型执行任何操作
// 这种机制起到了很强的预防性,更安全,这就要求我们必须缩小类型,我们可以使用typeof、类型断言等方式来缩小未知范围
if (typeof data === "number") {
let sum = num + data;
console.log(sum);
}

unknownany一样,所有类型都可以分配给unknown

但是任何类型的值可以赋值给any,同时any类型的值也可以赋值给任何类型。unknown 任何类型的值都可以赋值给它,但它只能赋值给unknownany

包装类型

Number、String、Boolean、Symbol被称为包装类型,当然这是我自己起的名字

原始类型numberstringbooleansymbol 混淆的首字母大写的 NumberStringBooleanSymbol 类型,后者是相应原始类型的包装对象

从类型兼容性上看,原始类型兼容对应的对象类型,反过来对象类型不兼容对应的原始类型。

1
2
3
4
let num: number;
let Num: Number;
Num = num; // ok
num = Num; // ts(2322)报错

字面量类型

在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。

目前,TypeScript 支持 3 种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型,对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型,具体示例如下。

1
2
3
4
5
6
7
8
9
// 分别为字符串,数字,布尔字面量类型
let specifiedStr: 'this is string' = 'this is string'; // 类型是'this is string'
let specifiedNum: 1 = 1; // 类型是1
let specifiedBoolean: true = true; // 类型就是true

// 子类型
let str: string = 'any string';
specifiedStr = str; // ts(2322) 类型 '"string"' 不能赋值给类型 'this is string'
str = specifiedStr; // ok

当然单单使用一个字面量类型没啥意义,它真正的应用场景是可以把多个字面量类型组合成一个联合类型

1
2
// 一个元素只能为1 2 3的数组
let arr: Array<1 | 2 | 3> = [1, 2, 3]

联合类型

联合类型表示取值可以为多种类型中的一种,使用 | 分隔每个类型。你可以把它当成是求交集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 表明myFavoriteNumber为string 或者 number
let myFavoriteNumber
myFavoriteNumber: string | number;
myFavoriteNumber = 'seven'; // OK
myFavoriteNumber = 7; // OK

// 与undefined和null联用
const sayHello = (name: string | undefined) => {
/* ... */
};

// 和字面量一起使用
let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';

// 联合类型只能 取到公共的方法或者属性
let n!: number | string;

交叉类型

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性,使用&定义交叉类型。

注意,如果我们仅仅把原始类型、字面量类型、函数类型等原子类型合并成交叉类型,是没有任何用处的,因为任何类型都不能满足同时属于多种原子类型,比如既是 string 类型又是 number 类型。因此,下面的代码中,类型别名 Useless 的类型是 never。

1
2
// never
type Useless = string & number;

交叉类型真正的用武之地就是将多个接口类型合并成一个类型,从而实现等同接口继承的效果,也就是所谓的合并接口类型,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 合并
type IntersectionType = { id: number; name: string; } & { age: number };
const mixed: IntersectionType = {
id: 1,
name: 'name',
age: 18
}



// 注意 &时会对同名属性进行合并
type Person = {
name: string,
age: number,
opt: {
walk: () => void;
}
}
type Animal = {
name: number,
age: number | string,
opt: {
eat: (food: string) => void;
}
}

type Sth = Person & Animal;
let o : Sth = {
// name的类型变为never 导致Sth类型无效
name: null,
// 类型为number
age: 16,
// 类型为{eat, walk}
opt: {
walk() {},
eat() {},
}
}

在上述示例中,我们通过交叉类型,使得IntersectionType同时拥有了 id、name、age 所有属性,这里我们可以试着将合并接口类型理解为求并集。

枚举类型

枚举通常用来约束某个变量的取值范围

解决了使用字面量进行类型约束的问题

  • 在类型约束位置会产生重复代码
  • 逻辑含义和真实的值产生了混淆,会导致当修改真实值的时候,产生大量修改
  • 字面量类型不会进入到编译结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 有重复代码
// 定义一个gender变量,并且约束为男或者女
let gender : "男" | "女";
// gender可以赋值为男或者女
gender = "女";
gender = "男";
//根据性别查询函数
function searchUsers(g:"男" | "女") {}


// 修改真实值的时候比较难改
type Gender = "帅哥" | "美女";

//定义一个gender变量,并且约束为男或者女
let gender : Gender;

//gender可以赋值为男或者女
gender = "女";
gender = "男";

//根据性别查询函数
function searchUsers(g:Gender) {}

这时候就可以使用枚举来优化

1
2
3
4
5
6
7
8
9
10
11
12
13
// 编译后的结果是一个对象
enum Gender {
male = "美女",
female = "帅哥"
}

let gender : Gender;
gender = Gender.male;
gender = Gender.female;

function searchUser(g: Gender) {}

searchUser(gender);

类型推断

类型推断:在很多情况下,TypeScript 会根据上下文环境自动推断出变量的类型

初始化值的变量有默认值的函数参数函数返回的类型都可以根据上下文推断出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 初始化值的变量 
let str = 'this is string'; // 自动标记为string类型
let num = 1; // 自动标记为number类型
let bool = true; // 自动标记为boolean类型

// 有默认值的函数参数
// 根据参数的类型,推断出返回值的类型也是 number
function add(a: number, b: number) {
return a + b;
}
const count = add(1, 1); // 推断出count的类型也是 number

// 推断参数 b 的类型是数字或者 undefined,返回值的类型也是数字
function add(a: number, b = 1) {
return a + b;
}

const count1 = add(1);
const count2 = add(1, '1'); // ts(2345) Argument of type "1" is not assignable to parameter of type 'number | undefined

如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查:

1
2
3
let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

类型断言

所以有时候你会遇到这样的情况,你会比TypeScript更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”

类型断言不是类型转换,它不会真的影响到变量的类型。

有时会碰到我们比TypeScript更清楚实际类型的情况,比如下面的例子:

1
2
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2); // 提示 ts(2322)

其中,greaterThan2 一定是一个数字,因为 arrayNumber 中明显有大于 2 的成员,但静态类型对运行时的逻辑无能为力。

TypeScript 看来,greaterThan2 的类型既可能是数字,也可能是 undefined,所以上面的示例中提示了一个 ts(2322) 错误,此时我们不能把类型 undefined 分配给类型 number

所以,这时候就需要类型断言了

强制类型断言

强制指定操作对象的类型

1
2
3
4
5
let n!: number | string;
// 尖括号
let num: number = <number>n;
// as 语法
let str: string = n as string;

非空断言

使用!断言操作对象是非 null 和非 undefined

1
2
3
let mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // Object is possibly 'null' or 'undefined'.

确认赋值断言

允许在实例属性和变量声明后面放置一个 ! 号,从而告诉 TypeScript 该属性会被明确地赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 未使用断言
let x: number;
initialize();

// Variable 'x' is used before being assigned.(2454)
console.log(2 * x); // Error
function initialize() {
x = 10;
}

// 使用断言
let x!: number;
initialize();
console.log(2 * x); // Ok

function initialize() {
x = 10;
}

双重断言

普通的断言是这样的

  • 联合类型可以被断言为其中一个类型
  • 父类可以被断言为子类

那,如果两个完全不兼容的断言,就要使用双重断言了,因为

  • 任何类型都可以被断言为 any
  • any 可以被断言为任何类型
1
2
3
4
5
// Conversion of type 'string | number' to type 'boolean' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
let bool: boolean = <boolean>n;

// 双重断言
let bool: boolean = n as any as boolean;

类型扩宽

所有通过 letvar 定义的变量、函数的形参、对象的非只读属性,如果满足指定了初始值且未显式添加类型注解的条件,那么它们推断出来的类型就是指定的初始值字面量类型拓宽后的类型,这就是字面量类型拓宽(Type Widening)。

1
2
3
4
5
6
7
8
9
10
11
12
// let变量,在缺省显式类型注解时,类型转换为了赋值字面量类型的父类型
let str = 'this is string'; // 类型是 string
let strFun = (str = 'this is string') => str; // 类型是 (str?: string) => string;

// 通过 let、var 定义的变量如果满足未显式声明类型注解且被赋予了 null 或 undefined 值,则推断出这些变量的类型是 any
let x = null; // 类型拓宽成 any
let y = undefined; // 类型拓宽成 any

// 将 const 定义为一个不可变更的常量,在缺省类型注解的情况下,TypeScript 推断出它的类型直接由赋值字面量的类型决定
const specifiedStr = 'this is string'; // 类型是 'this is string'
let str2 = specifiedStr; // 类型是 'string'
let strFun2 = (str = specifiedStr) => str; // 类型是 (str?: string) => string;

类型缩小

TypeScript中,我们可以通过某些操作将变量的类型由一个较为宽泛的集合缩小到相对较小、较明确的集合,这就是 “Type Narrowing”。

1
2
3
4
5
6
7
function func(anything: string | number) {
if (typeof anything === 'string') {
return anything; // 类型是 string
} else {
return anything; // 类型是 number
}
}

你可以使用下面几个方式进行类型缩小

  • 类型守卫:typeof
  • 类型判断:===
  • 控制流语句:if,三目运算符,switch分支

类型别名

类型别名用来给一个类型起个新名字

注意:类型别名,诚如其名,即我们仅仅是给类型取了一个新的名字,并不是创建了一个新的类型。

1
2
3
4
5
type Args = Array<string> | Array<number>;

function log(...args: Args) {
console.log(...args);
}

接口

在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。

什么是接口

在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。

TypeScript 中的接口是一个非常灵活的概念,除了可用于++「 对类的一部分行为进行抽象」++以外,也常用于对++「对象的形状(Shape)」++进行描述。

简单的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Person {
name: string;
age: number;
}

// 赋值的时候,变量的形状必须和接口的形状保持一致
// 多一个属性或者少一个属性都是不可以的
let tom: Person = {
name: 'Tom',
age: 25,
//  Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.
gender: 'male'
};

可选 & 只读属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Person {
// 全部属性都是只读的 不允许修改
// 其中gender是可选的变量
readonly name: string;
readonly age: number;
readonly gender ?: boolean;
}

let tom: Person = {
name: 'Sakura',
age: 16,
};

// Attempt to assign to const or readonly variable
tom.name = "Snow"

任意属性

有时候我们希望一个接口中除了包含必选和可选属性之外,还允许有其他的任意属性,这时我们可以使用 索引签名 的形式来满足上述要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Person {
readonly name: string;
readonly age: number;
readonly gender ?: boolean;
// 任意属性
[propName: string]: any;
}

let tom: Person = {
name: 'Sakura',
age: 16,
data: {}
};

需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集

也就是下面的代码是不允许的

1
2
3
4
5
6
7
8
interface Person {
readonly name: string;
// Property 'age' of type 'number' is not assignable to string index type 'string'.
readonly age: number;
readonly gender ?: boolean;
// 任意属性
[propName: string]: string;
}

接口和类型别名的区别

实际上,在大多数的情况下使用接口类型和类型别名的效果等价,但是在某些特定的场景下这两者还是存在很大区别。

TypeScript 的核心原则之一是对值所具有的结构进行类型检查。 而接口的作用就是为这些类型命名和为你的代码或第三方代码定义数据模型。

type(类型别名)会给一个类型起个新名字。 type 有时和 interface 很像,但是可以作用于原始值(基本类型),联合类型,元组以及其它任何你需要手写的类型。起别名不会新建一个类型 - 它创建了一个新 名字来引用那个类型。给基本类型起别名通常没什么用,尽管可以做为文档的一种形式使用。

image-20210204213714283

语法不同

两者都可以用来描述对象或函数的类型,但是语法不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// interface
interface Point {
x: number;
y: number;
}

interface SetPoint {
(x: number, y: number): void;
}

// type
type Point = {
x: number;
y: number;
};

type SetPoint = (x: number, y: number) => void;

接口可以自动合并

与类型别名不同,接口可以定义多次,会被自动合并为单个接口。

1
2
3
interface Point { x: number; }
interface Point { y: number; }
const point: Point = { x: 1, y: 2 };

利用这点可以对系统数据结构进行扩展

1
2
3
interface Array<T> {
remove(index : number) : Array<T>
}

接口可以在声明函数类型时同时声明一些属性

1
2
3
4
5
6
7
8
9
10
11
12
13
// 无法再声明其他的属性
type Type = (num : number) => number;

interface IType {
(num : number) : void;
_cache : Array<number>
}

const foo : IType = function () {

}

foo._cache = []

我也很少用,也就在刷leetcode时写斐波那契数列时用过

type可以声明一些interface无法表示的类型,比如union和tuple

1
2
3
4
5
// union
type PartialPoint = PartialPointX | PartialPointY;

// tuple
type Data = [number, string];

扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 接口扩展接口
interface PointX {
x: number
}

interface Point extends PointX {
y: number
}

// 类型别名扩展类型别名

type PointX = {
x: number
}

type Point = PointX & {
y: number
}

// 接口扩展类型别名
type PointX = {
x: number
}
interface Point extends PointX {
y: number
}

// 类型别名扩展接口
interface PointX {
x: number
}
type Point = PointX & {
y: number
}

传统的面向对象语言都是基于类的,而JavaScript是基于原型的。在ES6中拥有了class关键字,虽然它的本质依旧是构造函数,但是能够让开发者更舒服的使用class了。 TypeScript 作为 JavaScript 的超集,自然也是支持 class 全部特性的,并且还可以对类的属性、方法等进行静态类型检测。

基本概念

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface PointInterface {
x: number;
y: number;
}

class Point implements PointInterface {
x: number;
y: number;

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

// 注意,方法是直接设置在Point.prototype上的
getPosition() {
return `(${this.x}, ${this.y})`;
}
}

const point = new Point(1, 2);
point.getPosition() // (1, 2)

继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
eat() {
console.log("eat");
}
}

// 使用extend继承
class Cat extends Animal {
age: number;
constructor(name: string, age: number) {
super(name);
this.age = age;
}
play() {
console.log("play");
}
}

如上,Cat继承Animal,那Animal被称为父类(超类),Cat被称为子类(派生类)。此时Cat的实例继承了基类Animal的属性和方法。

需要注意,派生类如果包含一个构造函数constructor,则必须在构造函数中调用 super() 方法,这是 TypeScript 强制执行的一条重要规则。否则就会报错:Constructors for derived classes must contain a 'super' call.

那这个 super() 有什么作用呢?其实这里的 super 函数会调用基类的构造函数,用于数据初始化。

类的修饰符

在 ES6 标准类的定义中,默认情况下,定义在实例的属性和方法会在创建实例后添加到实例上;

而如果是定义在类里没有定义在this上的方法,实例可以继承这个方法;而如果使用 static 修饰符定义的属性和方法,是静态属性和静态方法,实例是没法访问和继承到的。

传统面向对象语言通常都有访问修饰符,可以通过修饰符来控制可访问性。TypeScript 中有三类访问修饰符:

  • public:修饰的是在任何地方可见、公有的属性或方法;
  • private:修饰的是仅在同一类中可见、私有的属性或方法;
  • protected:修饰的是仅在类自身及子类中可见、受保护的属性或方法。

此外,在类中可以使用readonly关键字将属性设置为只读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Stack<T> {
// 私有属性
private readonly arr: Array<T> = [];
// 静态属性
public static readonly Name = "Stack";
public static isStack(o: any): o is Stack<any> {
return o instanceof Stack;
}
// 访问器属性
get length() {
return this.arr.length;
}
// 方法
public push(...args: Array<T>) {
this.arr.push(...args);
}
public pop(): T {
let v = this.arr[this.length - 1];
this.arr.length--;
return v;
}
public isEmpty() {
return !this.length;
}
protected forEach(fun: Function) {
let cache = this.arr.splice(0, this.arr.length);
cache.forEach((value) => {
fun(value);
})
}
}

let stack = new Stack<number>();
stack.push(1);
stack.push(2);

console.log(stack.pop());
console.log(stack.pop());
console.log(stack.length);

console.log(Stack.isStack(stack));
console.log(Stack.Name);

抽象类

抽象类是一种不能被实例化的类,它的目的就是用来继承的,抽象类里面可以有抽象的成员,就是自己不实现,等着子类去实现。

  • 抽象类和抽象成员,都是用abstract修饰
  • 抽象类中还是可以有具体实现的,这样子类如果不实现,可以继承抽象类中的实现。
1
2
3
4
5
6
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log('roaming the earch...');
}
}

泛型

泛型的定义

软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。

举个例子

1
2
3
4
5
6
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}

console.log(identity<Number, string>(68, "Semlinker"));

1729b3dbccc38ea7_tplv-t2oaga2asx-zoom-in-crop-mark_1304_0_0_0

1729b3d9773f34ad_tplv-t2oaga2asx-zoom-in-crop-mark_1304_0_0_0

其中 T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:

  • K(Key):表示对象中的键类型;
  • V(Value):表示对象中的值类型;
  • E(Element):表示元素类型。

泛型的使用

在接口中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Identities<V, M> {
value: V,
message: M
}

function identity<T, U> (value: T, message: U): Identities<T, U> {
console.log(value + ": " + typeof (value));
console.log(message + ": " + typeof (message));
let res: Identities<T, U> = {
value,
message
};
return res;
}

// ts自动推断类型了
console.log(identity(16, "Sakura"));
// 写全是这样的
console.log(identity<number, string>(16, "Sakura"))

在类中使用

直接拿Array举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
interface Array<T> {
/**
* Gets or sets the length of the array. This is a number one higher than the highest element defined in an array.
*/
length: number;
/**
* Removes the last element from an array and returns it.
*/
pop(): T | undefined;
/**
* Appends new elements to an array, and returns the new length of the array.
* @param items New elements of the Array.
*/
push(...items: T[]): number;
/**
* Calls a defined callback function on each element of an array, and returns an array that contains the results.
* @param callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.
* @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
*/
map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
}

interface ArrayConstructor {
new(arrayLength?: number): any[];
<T>(arrayLength: number): T[];
isArray(arg: any): arg is any[];
readonly prototype: any[];
}

declare var Array: ArrayConstructor;

用法

1
2
3
// 大伙都很熟悉 
let arr : Array<number> = new Array<number>();
arr.push(1);

泛型约束

泛型的一个应用场景是确保属性存在,有时候,我们希望类型变量对应的类型上存在某些属性。这时,除非我们显式地将特定属性定义为类型变量,否则编译器不会知道它们的存在。

1
2
3
4
5
6
7
8
9
interface Length {
length: number;
}

// T extends Length用于告诉编译器,我们支持已经实现Length接口的任何类型。
function identity<T extends Length>(arg: T): T {
console.log(arg.length); // 可以获取length属性
return arg;
}

泛型约束的另一个常见的使用场景就是检查对象上的键是否存在。不过在看具体示例之前,我们得来了解一下 keyof 操作符,**keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。**

1
2
3
4
5
6
7
8
9
interface Person {
name: string;
age: number;
location: string;
}

type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[]; // number | "length" | "push" | "concat" | ...
type K3 = keyof { [x: string]: Person }; // string | number

通过 keyof 操作符,我们就可以获取指定类型的所有键,之后我们就可以结合前面介绍的 extends 约束,即限制输入的属性名包含在 keyof 返回的联合类型中。

1
2
3
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

泛型参数默认类型

一看就懂,和函数一样的

1
2
3
4
5
6
interface User<T = string> {
name: T;
}

const strA: User = {name: "Sakura"};
const numB: User<number> = {name: 101};

高级语法 / 用法

typeof

typeof 的主要用途是在类型上下文中获取变量或者属性的类型

1
2
3
4
5
6
interface Person {
name: string;
age: number;
}
const sakura: Person = { name: "Sakura", age: 16 };
type SakuraType = typeof sakura; // type SakuraType = Person

keyof

keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。

为了同时支持两种索引类型,就得要求数字索引的返回值必须是字符串索引返回值的子类。其中的原因就是当使用数值索引时,JavaScript 在执行索引操作时,会先把数值索引先转换为字符串索引。所以 keyof { [x: string]: Person } 的结果会返回 string | number

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface Person {
name: string;
age: number;
}

type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
type K3 = keyof { [x: string]: Person }; // string | number


interface StringArray {
// 字符串索引 -> keyof StringArray => string | number
[index: string]: string;
}

interface StringArray1 {
// 数字索引 -> keyof StringArray1 => number
[index: number]: string;
}

// keyof也支持基本数据类型
let K1: keyof boolean; // let K1: "valueOf"
let K2: keyof number; // let K2: "toString" | "toFixed" | "toExponential" | ...
let K3: keyof symbol; // let K1: "valueOf"

in

in 用来遍历联合类型,主要用于数组和对象的构建

1
2
3
4
5
type Keys = "a" | "b" | "c"

type Obj = {
[p in Keys]: any
} // -> { a: any, b: any, c: any }

infer

infer用于提取那个位置的类型值

1
2
3
4
5
6
7
8
9
10
11
function getSchool(name: string, age: number, address: string) {
return {name, age, address}
}

// T 是getSchool的类型 。 T extends ((...args: any[]) => infer R)
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any
type MyReturnType = ReturnType<typeof getSchool>; // 这个类型很好用


type Parameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never
type MyParamaters = Parameters<typeof getSchool>

以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。

索引类型

1
2
3
4
5
6
7
8
9
// T[K]表示对象T的属性K所表示的类型,在上述例子中,T[K][] 表示变量T取属性K的值的数组

// 通过[]索引类型访问操作符, 我们就能得到某个索引的类型
class Person {
name: string;
age: number;
}

type MyType = Person['name']; //Person中name的类型为string, 所以type MyType = string

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Person {
name: string;
age: number;
}

const person: Person = {
name: 'sakura',
age: 16
}

function getValues<T, K extends keyof T>(person: T, keys: K[]): T[K][] {
return keys.map(key => person[key]);
}

getValues(person, ['name']) // ['sakura']
getValues(person, ['gender']) // 报错: Type "gender" is not assignable to type "name" | "age".

映射类型

根据旧的类型创建出新的类型, 我们称之为映射类型

常见的有

Partial

Partial<T> 将类型的属性变成可选

1
2
3
4
5
6
type Partial<T> = {
[P in keyof T]?: T[P];
};

// Initial type: {name?: string, age?: number}
type PartialPerson = Partial<Person>;

Required

Required将类型的属性变成必选

1
2
3
type Required<T> = {
[P in keyof T]-?: T[P]
};

Readonly

Readonly<T> 的作用是将某个类型所有属性变为只读属性,也就意味着这些属性不能被重新赋值。

1
2
3
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

Pick

Pick 从某个类型中挑出一些属性出来

1
2
3
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

Record

Record 的作用是将 K 中所有的属性的值转化为 T 类型。

1
2
3
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

ReturnType

用来得到一个函数的返回值类型

理解为:如果 T 继承了 extends (...args: any[]) => any 类型,则返回类型 R,否则返回 any。其中 R 是什么呢?R 被定义在 extends (...args: any[]) => infer R 中,即 R 是从传入参数类型中推导出来的。

1
2
type ReturnType<T extends (...args: any[]) => any> = 
T extends (...args: any[]) => infer R ? R : any;

Parameters

Parameters<T> 的作用是用于获得函数的参数类型组成的元组类型。

1
2
type Parameters<T extends (...args: any) => any> = 
T extends (...args: infer P) => any ? P : never;

Exclude

Exclude 的作用是将某个类型中属于另一个的类型移除掉。

1
2
// 因为extends是分配式的,所以这么写是ok的
type Exclude<T, U> = T extends U ? never : T;

如果 T 能赋值给 U 类型的话,那么就会返回 never 类型,否则返回 T 类型。最终实现的效果就是将 T 中某些属于 U 的类型移除掉。

用法

1
2
3
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number

Extract

Extract<T, U> 的作用是从 T 中提取出 U

1
type Extract<T, U> = T extends U ? T : never;

用法

1
2
type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
type T1 = Extract<string | number | (() => void), Function>; // () =>void

Omit

Omit<T, K extends keyof any> 的作用是使用 T 类型中除了 K 类型的所有属性,来构造一个新的类型。

1
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

用法

1
2
3
4
5
6
7
8
9
10
11
12
interface Todo {
title: string;
description: string;
completed: boolean;
}

type TodoPreview = Omit<Todo, "description">;

const todo: TodoPreview = {
title: "Clean room",
completed: false,
};

NonNullable

NonNullable<T> 的作用是用来过滤类型中的 nullundefined 类型。

1
type NonNullable<T> = T extends null | undefined ? never : T;

用法

1
2
type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]

其他

tsconfig.json的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
{
"compilerOptions": {

/* 基本选项 */
"target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
"module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
"lib": [], // 指定要包含在编译中的库文件
"allowJs": true, // 允许编译 javascript 文件
"checkJs": true, // 报告 javascript 文件中的错误
"jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
"declaration": true, // 生成相应的 '.d.ts' 文件
"sourceMap": true, // 生成相应的 '.map' 文件
"outFile": "./", // 将输出文件合并为一个文件
"outDir": "./", // 指定输出目录
"rootDir": "./", // 用来控制输出目录结构 --outDir.
"removeComments": true, // 删除编译后的所有的注释
"noEmit": true, // 不生成输出文件
"importHelpers": true, // 从 tslib 导入辅助工具函数
"isolatedModules": true, // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).

/* 严格的类型检查选项 */
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
"strictNullChecks": true, // 启用严格的 null 检查
"noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
"alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

/* 额外的检查 */
"noUnusedLocals": true, // 有未使用的变量时,抛出错误
"noUnusedParameters": true, // 有未使用的参数时,抛出错误
"noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
"noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)

/* 模块解析选项 */
"moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
"baseUrl": "./", // 用于解析非相对模块名称的基目录
"paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
"rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
"typeRoots": [], // 包含类型声明的文件列表
"types": [], // 需要包含的类型声明文件名列表
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。

/* Source Map Options */
"sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
"mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
"inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
"inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

/* 其他选项 */
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true // 为装饰器提供元数据的支持
}
}

参考

https://juejin.cn/post/7050290769562697736

https://juejin.cn/post/6844904184894980104

https://juejin.cn/post/7000182870404759589

https://blog.csdn.net/lhjuejiang/article/details/119038312

https://juejin.cn/post/6999441997236797470

https://juejin.cn/post/6844904146877808653

https://juejin.cn/post/6844904146877808653