TypeScript入坑指北

类型

Typescript 包含 Javascript 的全部类型,也就是 number、boolean、string、object、bigint、symbol、undefined、null,还有包装类 Number、Boolean、String、Object、Symbol。

除此之外,Typescript 还有 void、never、enum、unknown、any、tuple(元组) 以及自定义的 type 和 interface。

null、undefined

在 Typescript 中,null、undefined 标识一个具体的类型值,被当成其他类型的子类型(在不开启 strictNullChecks 前提下),也就是说 null 和 undefined 可以赋值给其他类型的变量

void

用来表示一个函数没有显示的返回值。这里的 test2 虽然可以用 void 但其实它显示声明了 return, 只不过什么都没返回,所以用 undefined 更好。

1
2
3
4
5
function test1(): void {}
// 或者
function test2(): void {
return;
}

void 的特殊情况

1
2
3
type Fun  = () => void;
const fun: Fun = () => 'hello world';
const result = fun();

这里 Fun 返回值是 void,实际 fun 返回了一个字符串,这段代码并没有报错,而且 result 的类型还是 void。这虽然看起来有点奇怪,但其实 TS 并没有强制要求 void 真的不能什么都不返回,他的意思更倾向于返回值并不会被使用,就像我们写 forEach 返回值是 void ,但我们箭头函数简写一般都会省略代码块。

1
[1,2,3].forEach((item) => item + 1);

never

never 在 Typescript 中表示什么都没有,他不可以被任何类型赋值,但可以赋值给任何类型。
不能被任何类型赋值
另外在联合类型中,never 直接被无视掉。
在联合类型被移除

枚举 enum

枚举其实可以理解成我们把一些常量封装到一个命名空间里面,通过这个命名空间来给我们提供类型提示。
另外,如果你没有给你的 enum 赋值,那么他会自动从 0 开始累加赋值。如果你给某个枚举赋值数字,那么他下面的会依次累加。

1
2
3
4
5
6
enum Numbers {
a, // 0
b, // 1
c = 200,
d, // 201
}

enum 的延迟求值

在枚举中,可以通过赋值一个函数执行来进行延迟求值,但是如果使用延迟求职,他后面的值无法自己累加,所以没显示赋值的枚举不能直接在延迟求职之后。

1
2
3
4
5
6
7
const getNum = () => 1 + 1;

enum Items {
num1 = getNum(),
num2 = 1,
num3,
}

双向映射

Typescript 中,值为数字的枚举成员是双向映射的,也就是你可以通过枚举成员获取到枚举值,也可以通过枚举值来找到它的枚举成员,延迟求职的枚举也不例外。

1
2
3
4
5
6
7
8
9
10
const getNum = () => 1 + 14;

enum Items {
num1 = getNum(),
num2 = 1,
num3,
}

const num = Items.num1 // 15
const key = Items[15] // 'num1'

常量枚举

常量枚举只能通过枚举成员访问枚举值,它的编译产物和普通枚举也有所不同。

1
2
3
4
5
6
7
8
// 常量枚举
const enum Items {
num1,
num2,
num3,
}

const num = Items.num1

常量枚举编译产物 在线试一试

1
var num = 0 /* num1 */;

普通枚举产物 在线试一试

1
2
3
4
5
6
7
var Items;
(function (Items) {
Items[Items["num1"] = 0] = "num1";
Items[Items["num2"] = 1] = "num2";
Items[Items["num3"] = 2] = "num3";
})(Items || (Items = {}));
var num = Items.num1;

通过产物不难看出,普通枚举双向映射的原因是进行了两次赋值,在 Item 中对 枚举值和枚举对象都进行了赋值。

元组 Tuple

元组类型其实就是元素个数和类型固定的数组类型

1
type Tuple = [number, string]

接口 interface

接口一般用来描述对象、函数、构造器、索引类型(对象、class、数组)等。

1
2
3
4
5
interface IPerson {
name: string;
age: number;
email: string;
}

接口可以被继承

1
2
3
4
5
6
7
8
9
interface IPerson2 extends IPerson {
phone: string;
}
const peron:IPerson2 = {
name: 'john',
age: 18,
email: 'xxx.xx.com',
phone: '1xxxxxxxxx'
}

还可以被实现

1
2
3
4
5
class Person implements IPerson {
age: number;
email: string;
name: string;
}

接口索引签名

当遇到一个对象有多个属性,并且属性名不确定的情况,可以通过索引签名来处理未知的属性

1
2
3
4
5
6
7
interface IPerson {
[prop: string]: string;
}
const person: IPerson = {
name: 'john',
phone: 'xxxxxx',
}

interface 和 type 区别

  1. type 能描述所有数据,interface 描述对象
  2. interface 可以同名(视为扩展),type 不能重名
  3. interface 可以继承(extends)和实现(implements)
  4. interface 可以声明属性为可选或必填,type 不行。

类型装饰

是否可选

Typescript 中通过 ? 来声明一个属性是否为可选属性

1
2
3
4
5
6
7
8
interface IPerson {
name: string;
age?: number;
}
const person: IPerson = {
name: 'john',
// age 为可选属性 非必选项
}

是否只读

Typescript 通过 readonly 来声明一个值是只读的,当只读的属性被修改报错。
readonly.png

类型断言

类型断言就是在 Typescript 分析的类型不符合预期时,自己手动指定一个类型。

尖括号方法

1
2
let value: any = 'hello world';
let len: number = (<string>value).length;

通过尖括号的方法来将 any 类型的 value 断言为 string 类型,调用 string 的 length 属性

as

1
2
let value: any = 'hello world';
let len: number = (value as string).length;

as 语法进行断言应该更加常用。

非空断言

非空断言用来声明某个值一定不是 null 或 undefined。

这里的 name 可能不存在 ,所以 Typescript 警告属性可能为 undefined,此时我们就可以使用非空断言,声明值存在。

1
2
3
4
5
6
7
type Person = {
name?: string,
}
const person:Person = {
name: 'john',
}
console.log(person.name!.length)

非空断言在处理某些属性为空值无法继续调用时很有用。

类型运算

条件类型 extends

extends 一般用作泛型类型约束,和定义条件类型

  1. 泛型类型约束
    1
    2
    3
    type ToReadonly<T extends object> = {
    readonly [Key in keyof T]: T[Key]
    }
    泛型类型约束就是限制传入参数的类型
  2. 定义条件类型
    1
    type IsAny<T> = 'a' extends ('b' & T) ? true : false
    定义条件类型就是在做类型运算的时候,对类型进行推导,可以简单理解为 extends 前的类型是否为后的子类型,然后通过 Javascript 里常用的三元表达式来得到计算结果。

推导 infer

infer 在做类型运算的时候,起到提取元素的作用,你也可以简单理解为,用 infer 声明一个变量,然后在计算中使用这个变量。

1
type reverseArr<T extends unknown[]> = T extends [...infer Fist, infer Last] ? [Last, ...reverseArr<Fist>] : T;

联合 |

和 Javascript 中的 | 作用类似,代表满足多个类型中的一个即可。

1
2
type NumType = 1 | 2 | 3;
const num:NumType = 1;

交叉 &

和 Javascript 中的 & 作用类似,代表需要满足全部条件,对类型做合并。

1
2
3
4
5
6
type Type = { name: string } & { age: number }

const person:Type = {
name: 'john',
age: 23,
}

另外不同类型无法合并

映射类型

映射类型用于从一个现有的类型中创建新类型的方式,就是在在现有类型基础上进行属性转换

1
2
3
type MapType<T> = {
[Key in keyof T]?: T[Key]
}

in 就是对 keyof T 进行遍历
keyof 是取 T 的key,可以成为索引查询
T[Key] 是取值,成为索引访问

那么通过现有类型如何创建新类型呢

1
2
3
type MapType<T> = {
[Key in keyof T]?: [T[Key], T[Key]]
}

首先可以对索引的 value 进行修改,这里让每个值变成相同类型的长度为2的数组

1
2
3
type MapType<T> = {
[Key in keyof T as Key extends `${infer A}${infer B}` ? `${A}_${B}` : never]?: [T[Key], T[Key]]
}

索引当然也可以修改,通过 as 做重映射,重新定义索引名。

1
2
3
4
const test:MapType<{ab: number, cd: number}> = {
a_b: [1,2],
c_d: [3,4],
}

类型守卫

先看一段代码

1
2
3
4
5
6
7
function processValue(value: unknown) {
if (typeof value === "string") {
console.log(value.toUpperCase());
} else {
console.log('Value is not a string.');
}
}

这段代码我们判断入参 value 是否为 string 类型,这时在 if 内 value 已经是 string 类型。所以可以调用字符串的 toUpperCase 方法,下面我们把校验的过程抽离出来。

1
2
3
4
5
6
7
8
9
10
11
function isString(value: unknown): boolean {
return typeof value === 'string';
}

function processValue(value: unknown) {
if (isString(value)) {
console.log(value.toUpperCase());
} else {
console.log('Value is not a string.');
}
}

这时发现类型错误,value 还是 unknown 并没有因为校验而更改类型。

Typescript 通过 is 关键字来处理这种情况

1
2
3
function isString(value: unknown): value is string {
return typeof value === 'string';
}

isString 这种方法称之为类型守卫,想要通过类型守卫获得参数类型,需要通过 参数 + is + 预期类型来显示声明,这样才能在判断中矫正参数类型。

使用 is 也有一些注意的地方

  • 必须返回一个布尔值。
  • 通常结合 if、else 等条件语句使用,以根据类型进行不同的逻辑处理。
  • 使用 is 编译器会智能推断变量类型,就不用再显示声明了

类型系统

结构化类型系统

TypeScript 的类型系统是一种结构化类型系统,那什么是结构化类型系统?看看 ai 怎么说:
结构化类型系统

好的,没看懂,下面用代码来体验一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Father {
name: string;
}
interface Son {
name: string;
age: number;
}
const son: Son = {
name: 'xiaoming',
age: 18,
}
function test(param:Father){}
test(son);

test 方法的参数明明是 Father 类型,但我们传入的 Son 类型却没有报错,这是因为这两个接口都有相同的 name 属性,并且 Son 在 Father 的基础上多了一个 age 而已,结构化类型系统会认为 Son 是 Father 的子类 。这也就是上图中的如果两个类型的成员结构相同(即它们拥有相同的属性和方法),那么它们被认为是兼容的。另外这种子类型可以赋值给父类型的情况叫做协变

分布式类型

分布式类型
图中代码使用联合类型作为参数,Typescript 遇到这种情况会把联合类型分别传入进行类型运算,然后再合并成新的联合类型,这就是分布式类型。

1
2
3
4
5
6
type Test<T extends string, Y extends string> =
T extends Y
? true
: false;

type T = Test<'a' | 'b', 'a'>

正常情况下 'a' | 'b' extends 'a' 结果应该为 false,但当我们把他们分别当作参数传入 Test 的时候,结果却发生了变化

结果变成了 boolean 这就是因为分布式的作用所致,当我们把 'a'|'b' 传入时,等同于分别传入 'a''b' 得到结果后再联合,'a' 得到的结果是 true,'b'得到的结果是 false,联合起来就是 boolean 类型。

关闭分布式

以上得到的结果显然是不对的,那我们如何关闭分布式,让他直接对比呢

1
2
3
4
5
6
type Test<T extends string, Y extends string> =
[T] extends [Y]
? true
: false;

type T = Test<'a' | 'b', 'a'> // false

这里把 T extends Y 改成 [T] extends [Y] 关闭分布式,让联合类型不再分别做类型运算,而是直接进行运算,这样就可以直接运算得到符合预期的结果了。

内置高级类型

  • Parameters
  • ReturnType
  • ConstructorParameters
  • InstanceType
  • ThisParameterType
  • OmitThisParameter
  • Partial
  • Required
  • Readonly
  • Pick
  • Record
  • Exclude
  • Extract
  • Omit
  • Awaited
  • NonNullable
  • Uppercase、Lowercase、Capitalize、Uncapitaliz*

TypeScript入坑指北
https://l1ushun.github.io/2023/10/31/ts-01/
作者
liu shun
发布于
2023年10月31日