教程中的难点2
提示
本文整理自 TypeScript
官方文档中的 handlebook,版本为 V5.4
# 对象的类型
- 对象的类型可以为匿名、类型别名、接口定义的
function fn(options: {
childEle: HTMLElement
parentEle: Element
easing: (t: number) => number
duration: number | "auto"
callback?: () => void
}): void {} // options 的类型是直接定义的,即匿名的
type FnOptions = { // 使用类型别名定义
childEle: HTMLElement
parentEle: Element
easing: (t: number) => number
duration: number | "auto"
callback?: () => void
}
interface FnOptionApi { // 使用接口定义
childEle: HTMLElement
parentEle: Element
easing: (t: number) => number
duration: number | "auto"
callback?: () => void
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 类型中可以加入可选、只读、索引签名
- 可选
type Person = {
name: string
age?: boolean // 实际上相当于设置 age 的属性为 boolean | undefined
}
2
3
4
- 只读
type Person = {
name: string
readonly age: boolean // 在属性名前面加上 readonly 标志符将会使属性变为只读的
}
const person: Person = {
name: 'joker',
age: 20 // age 属性不可更改,只允许读取
}
console.log(person.age) // 20
person.age = 21 // setter 被禁用了,此时将会报错
/**
* @description ts 判断类型是否兼容时不会观察 readonly,可以通过这个特性将 readonly 变为可以修改的
*/
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let writablePerson: Person = {
name: "Person McPersonface",
age: 42,
};
// works
let readonlyPerson: ReadonlyPerson = writablePerson;
console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'
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
- 类型修饰
/**
* @description 类型索引可以帮助我们在不知道确切属性名的情况下对类型作出一定限制
*/
// 例如当前并不知道数组哪些位置上有值,但是知道时数组,并且对应的值都为
interface StringArray {
[index: number]: string
}
const arr: StringArray = ['1', '2', '3']
// 索引签名属性允许 string number symbol 模版字符串 或者以上四种的组合
interface Person {
name: string
age: number
[action: number | symbol]: () => void
}
const xiaoming: Person = { name: 'xiaoming', age: 20 }
const fnSymbol = Symbol('小明会跑')
xiaoming[fnSymbol] = () => { console.log('小明开始跑了') }
xiaoming[fnSymbol]()
// 索引签名可以设置 string | number | symbol 但是如果之前已经声明了相关类型,需要与之兼容
interface Dog {
name: string
age: number
[action: string]: string | number // 只有这样才是与上面两种兼容的,或者将属性名的类型换成 number | symbol,这样才可以赋其他值
}
// 在索引签名中,也可以加入 readonly,只允许初始化时赋值,之后不可修改
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
- 使用 mapping 操作符可以去除可选和只读修饰符
/**
* @description 使用 mapping 可以去除可选与只读的命令
*/
interface initialType {
name?: string
readonly age: number
readonly [index: number]: string
[action: symbol]: () => void
}
// 使用类型别名复制原来的类型
type copiedType = {
[T in keyof initialType]: initialType[T]
}
// 使用类型别名去除可选属性的可选修饰符 name
type requiredType = {
[T in keyof initialType]-?: initialType[T]
}
// 使用类型别名去除只读修饰符
type modifiableType = {
-readonly [T in keyof initialType]: initialType[T]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- 在
TypeScript
4.1 以上,可以使用 as 操作符处理映射类型
/**
* @description 使用 as 操作符来操作映射
*/
type Getters<T> = {
[Property in keyof T as `get${Capitalize<string & Property>}`]: () => T[Property]
}
interface Person {
name: string
age: number
location: string
}
type LazyPerson = Getters<Person>
// 可以通过将属性名的类型设置为 never 来过滤一些属性
type RemoveKindField<T> = {
[Property in keyof T as Exclude<Property, "kind">]: T[Property]
}
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
// 可以联合的类型不只是 string | numebr | symbol
type EventConfig<Events extends { kind: string }> = {
[E in Events as E["kind"]]: (event: E) => void;
}
type SquareEvent = { kind: "square", x: number, y: number };
type CircleEvent = { kind: "circle", radius: number };
type Config = EventConfig<SquareEvent | CircleEvent>
// 进阶类型操作
type ExtractPII<Type> = {
[Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
};
type DBFields = {
id: { format: "incrementing" };
name: { type: string; pii: true };
};
type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>;
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
- interface 与 type alias 都可以扩展类型,但是处理冲突的方式不一样
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
interface AddressWithUnit extends BasicAddress {
unit: string;
}
interface Colorful1 {
color: string;
}
interface Circle1 {
radius: number;
}
interface ColorfulCircle extends Colorful1, Circle1 {}
type ColorfulCircle2 = Colorful1 & Circle1
const cc: ColorfulCircle = {
color: "red",
radius: 42,
};
const cc2: ColorfulCircle = {
color: "red",
radius: 42,
};
// 同一属性类型冲突
interface CircleArea {
kind: 'circle'
radius: number
getArea: (radius: number) => number
}
interface rectArea {
kind: 'rect'
width: number
getArea: (width: number) => number
}
interface newArea extends CircleArea, rectArea {} // 冲突后提示不能
type newArea2 = CircleArea & rectArea // 变为 never,但是不会报错
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
- 当能使用泛型时,优先使用泛型,而不是函数重载
- 数组和只读数组的定义就使用了泛型
Array
和ReadonlyArray
Array
类型的值可以赋值给ReadonlyArray
类型,反之则无法进行
- 元组类型定义了每个位置上的值的类型
- 元组的可选操作符只能放在最后一位
- 元组可以使用展开运算符,将对应的数组和元组类型加入到里面,例如
type StringNumberBooleans = [string, number, ...boolean[]]
- 假设定义了某个元组并设置为 as const 将其类型设置为只读的元组是类似的
# 类型体操
# 泛型
function Identity<T>(arg: T): T {
return arg
}
function numberIdentity (arg: number): number {
return arg
}
interface GenericIdentityFn<T> {
(arg: T): T
}
type IdentityFn<T> = (arg: T) => T
const myIdentity: <T>(arg: T) => T = Identity // 箭头函数形式
const myIdentity2: { <T>(arg: T): T } = Identity // 对象形式
const knownIdentity: GenericIdentityFn<number> = Identity // 加入 <number> 后,对应函数传入的参数的类型就已知了,不再是泛型了
// 在调用泛型接口和类型别名时,必须传入对应的参数
const numberIdentity1: GenericIdentityFn = Identity
const numberIdentity4: IdentityFn = Identity
const numberIdentity2: GenericIdentityFn<string> = Identity
const numberIdentity3: GenericIdentityFn<string> = numberIdentity // 此时类型不匹配
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- 类也有泛型的概念
class GenericNumber<NumType> {
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
return x + y;
};
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
泛型类仅在实例端有泛型,在静态端无法应用泛型
通用约束
interface LengthWise {
length: number
}
2
3
- 在泛型约束中也可以加入泛型
function getProperty<Type, Key extends keyof Type>(obj: Type, key: key) {
return obj[key]
}
2
3
- 通过构造函数来创建类的实例的过程中,必须通过类的构造函数来引用类类型
class BeeKeeper {
hasMask: boolean = true;
}
class ZooKeeper {
nametag: string = "Mikle";
}
class Animal {
numLegs: number = 4;
}
class Bee extends Animal {
numLegs = 6;
keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;
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
- 使用通用参数默认值可以简化函数重载的描述
declare function create(): Container<HTMLDivElement, HTMLDivElement[]>;
declare function create<T extends HTMLElement>(element: T): Container<T, T[]>;
declare function create<T extends HTMLElement, U extends HTMLElement>(
element: T,
children: U[]
): Container<T, U[]>;
2
3
4
5
6
declare function create<T extends HTMLElement = HTMLDivElement, U = T[]>(
element?: T,
children?: U
): Container<T, U>;
const div = create();
const p = create(new HTMLParagraphElement());
2
3
4
5
6
7
A generic parameter default follows the following rules: 泛型参数默认遵循以下规则:
A type parameter is deemed optional if it has a default. 如果类型参数具有默认值,则该参数被视为可选参数。
Required type parameters must not follow optional type parameters. 必需的类型参数不得遵循可选的类型参数。
Default types for a type parameter must satisfy the constraint for the type parameter, if it exists. 类型参数的默认类型必须满足类型参数的约束(如果存在)。
When specifying type arguments, you are only required to specify type arguments for the required type parameters. Unspecified type parameters will resolve to their default types. 指定类型参数时,只需为所需的类型参数指定类型参数。未指定的类型参数将解析为其默认类型。
If a default type is specified and inference cannot choose a candidate, the default type is inferred. 如果指定了默认类型,并且推理无法选择候选类型,则推断默认类型。 A class or interface declaration that merges with an existing class or interface declaration may introduce a default for an existing type parameter. 与现有类或接口声明合并的类或接口声明可能会为现有类型参数引入默认值。 A class or interface declaration that merges with an existing class or interface declaration may introduce a new type parameter as long as it specifies a default. 与现有类或接口声明合并的类或接口声明可以引入新的类型参数,只要它指定默认值即可。
# keyof 运算符
- keyof 运算符提取对象类型的键并生成对应的字符串和文本的联合。
type Point = { x: number; y: number };
type P = keyof Point;
2
- 如果类型具有 string 或 number 索引签名,则 keyof 将改为返回这些类型
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish; // type A = number
type Mapish = { [k: string]: boolean };
type M = keyof Mapish; // type M = string | number
2
3
4
5
- 请注意,在此示例中, M 是 string | number — 这是因为 JavaScript 对象键总是强制到字符串,因此 obj[0] 始终与 obj["0"] 相同。
# typeof 类型运算符
- 在 JS 中,typeof 可以返回一个变量的类型
const str = 'Hello world'
console.log(typeof str) // string
2
- TS 中加入了 typeof 运算符,可以使用 typeof 引用对应的变量或属性的类型
const s: string = 'Hello world'
const citedType: typeof s = '1'
2
- typeof 通常结合类型运算使用,对于基本类型来说作用不大
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<typeof f>; // f 是函数,typeof f 才是对应的类型
2
3
4
- ts有意限制了typeof的作用,只有对变量与属性使用typeof才有效,对表达式使用会报错
# 可以使用类型索引获取对象中的类型
interface eg {
name: string
age: number
}
type nameType = eg['name'] // 使用对应的键的类型可获取 string
type nameAndAgeType = eg['name' | 'age'] // 使用对应的键的类型可获取 string | number
// type noneType = eg['111'] // 不存在的属性类型将无法索引,会报错
const key = 'name'
// type nameType2 = eg[key] // 只能使用类型,不能使用变量
2
3
4
5
6
7
8
9
10
# 条件类型
- 条件类型类似于 js 中的三目运算符
interface Animal {
live(): void
}
interface Dog {
woof(): void
}
type Example1 = Dog extends Animal ? number : string // number
type Example2 = RegExp extends Animal ? number : string // string
2
3
4
5
6
7
8
9
10
- 条件类型与泛型结合使用
interface IdLabel {
id: number
}
interface nameLabel {
name: string
}
// 输入 id 返回包含number类型id属性的对象
function createLabel(id: number): IdLabel
// 输入 name 返回包含 string 类型name属性的对象
function createLabel(name: string): nameLabel
// 输入 可能为number 可能为string的对象时,两种返回都有可能
function createLabel(idOrName: number | string): IdLabel | nameLabel
function createLabel(idOrName: number | string): IdLabel | nameLabel {
throw 'unimplemented' // 表示函数尚未实现
}
// 使用条件类型可以简化以上操作
type NameOrId<T extends number | string> = T extends number ? IdLabel : nameLabel
function createLabel2<
T extends number | string // 保证 T 的类型为 string, number 或 string | number
>(idOrName: T): NameOrId<T> {
throw 'unimplemented'
}
const var1 = createLabel("typescript"); // nameLabel
const var2 = createLabel(2)
const var3 = createLabel(Math.random() ? 'hello' : 2024)
const v1 = createLabel2("javascript")
const v2 = createLabel2(2024)
const v3 = createLabel2(Math.random() ? 'yes' : 0)
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
- 条件类型约束
// type MessageOf<T> = T['message'] // 此时将会报错,因为T不一定有message名的类型
// 加入条件类型约束将会保证T有message
type MessageOf<T extends { message: unknown }> = T['message']
interface Email {
message: string
}
interface Dog {
bark(): void
}
type EmailMessageContents = MessageOf<Email> // string
// 使用条件类型将可以处理边界值
type MessageOf2<T> = T extends { message: unknown } ? T['message'] : never
type EmailMessageContents2 = MessageOf2<Email> // string
type DogMessageContent = MessageOf2<Dog> // never
// 数组扁平化的例子
type Flatten<T> = T extends any[] ? T[number] : T // 如果有数组,则返回元素的类型,否则返回T的类型
type Str = Flatten<string[]> // string
type StrOrNumber = Flatten<(string | number)[]> // string | number
type onlyNumber = Flatten<number> // number
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- infer 关键字:在条件类型中推断
// 重写 Flatten
type Flatten<T> = T extends Array<infer Item> ? Item : never
// 使用 never 表示函数不应该接受任何参数
type GetReturnType<T extends (...args: never[]) => any> = T extends (...args: never[]) => infer Return ? Return : never
type Num = GetReturnType<() => number> // number
type Str2 = GetReturnType<(x: string) => string> // string
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]> // boolean[]
2
3
4
5
6
7
8
9
- 在学习infer的用法时,发现typescript对函数类型中never的判断可能存在一定问题
type judge1 = ((x: string) => number) extends ((...args: never[]) => any) ? true : false // true
type judge2 = ((x: string) => number) extends ((arg: never) => any) ? true : false // true
type judge3 = ((x: string) => number) extends ((arg: number) => any) ? true : false // false
type judge4 = string extends never ? true : false // false
2
3
4
- 分配条件类型
// 当传入的类型参数为联合类型时,他们会被 分配类型
type ToArray<Type> = Type extends any ? Type[] : never;
// 如果我们将联合类型传入 ToArray,则条件类型将应用于该联合类型的每个成员
type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>; // string[] | number[]
// 通常,分布性是所需的行为。要避免这种行为,可以用方括号括起 extends 关键字的两边
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
// 'StrOrNumArr' 不再是一个联合类型
type StrOrNumArr = ToArrayNonDist<string | number>; // (string | number)[]
2
3
4
5
6
7
8
9
10
11
12
13
- 使用 mapping 操作符可以去除可选和只读修饰符
/**
* @description 使用 mapping 可以去除可选与只读的命令
*/
interface initialType {
name?: string
readonly age: number
readonly [index: number]: string
[action: symbol]: () => void
}
// 使用类型别名复制原来的类型
type copiedType = {
[T in keyof initialType]: initialType[T]
}
// 使用类型别名去除可选属性的可选修饰符 name
type requiredType = {
[T in keyof initialType]-?: initialType[T]
}
// 使用类型别名去除只读修饰符
type modifiableType = {
-readonly [T in keyof initialType]: initialType[T]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- 在
TypeScript
4.1 以上,可以使用 as 操作符处理映射类型
/**
* @description 使用 as 操作符来操作映射
*/
type Getters<T> = {
[Property in keyof T as `get${Capitalize<string & Property>}`]: () => T[Property]
}
interface Person {
name: string
age: number
location: string
}
type LazyPerson = Getters<Person>
// 可以通过将属性名的类型设置为 never 来过滤一些属性
type RemoveKindField<T> = {
[Property in keyof T as Exclude<Property, "kind">]: T[Property]
}
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
// 可以联合的类型不只是 string | numebr | symbol
type EventConfig<Events extends { kind: string }> = {
[E in Events as E["kind"]]: (event: E) => void;
}
type SquareEvent = { kind: "square", x: number, y: number };
type CircleEvent = { kind: "circle", radius: number };
type Config = EventConfig<SquareEvent | CircleEvent>
// 进阶类型操作
type ExtractPII<Type> = {
[Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
};
type DBFields = {
id: { format: "incrementing" };
name: { type: string; pii: true };
};
type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>;
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
# 模版字符串
- 模版字符串的简单应用
// 模版字符串可以进行简单的类型运算
type str1 = 'world'
type str2 = `hello ${str1}` // "hello world"
// 如果值不固定,将会计算所有组合
type str3 = 'lihua' | 'xiaomei'
type str4 = `i am ${str3}` // "i am lihua" | "i am xiaomei"
// 多个变量同理
// 结合模版字符串与keyof的例子
const passedObject = {
firstName: "Saoirse",
lastName: "Ronan",
age: 26,
};
type PropEventSource<Type> = {
on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26
});
person.on("firstNameChanged", () => {});
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
- 进一步优化例子
- 第一个参数中使用的文本被捕获为文本类型
- 可以验证该文本类型是否处于泛型中有效属性的并集中
- 可以使用索引访问在泛型的结构中查找已验证属性的类型
- 然后,可以应用此类型化信息来确保回调函数的参数属于同一类型
type PropEventSource<Type> = {
on<Key extends string & keyof Type>
(eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void): void;
};
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26
});
person.on("firstNameChanged", newName => {
// (parameter) newName: string
console.log(`new name is ${newName.toUpperCase()}`);
});
person.on("ageChanged", newAge => {
// (parameter) newAge: number
if (newAge < 0) {
console.warn("warning! negative age");
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- 内部字符串操作
- Uppercase 全大写
- Lowercase 全小写
- Capitalize 首字母大写
- Uncapitalize 首字母小写