面试题[TypeScript]
1. TypeScript 基础知识
1.1 TypeScript 和 JavaScript 之间的关系
TypeScript 和 JavaScript 是两种不同的编程语言,但它们之间存在着紧密的关系。以下是对它们之间关系的详细阐述:
- TypeScript 是 JavaScript 的超集
- TypeScript 是由 Microsoft 开发的一种自由和开源的编程语言,它扩展了 JavaScript 的语法和功能。
- 所有的合法 JavaScript 代码都是合法的 TypeScript 代码,这意味着 JavaScript 代码可以在 TypeScript 环境中无缝运行,而无需进行任何修改。
- TypeScript增加的新特性
- 静态类型检查:TypeScript 引入了静态类型系统,允许开发者在编写代码时指定变量的类型。这有助于在编译阶段发现潜在的错误,提高代码的可靠性和可维护性。
- 类、接口、命名空间和泛型:这些特性使得 TypeScript 支持面向对象的编程范式,提高了代码的可读性和可扩展性。
- TypeScript的编译过程
- TypeScript 代码需要被编译成 JavaScript 代码才能在浏览器中运行。这个编译过程是由 TypeScript 编译器完成的,它确保了 TypeScript 代码可以兼容任何支持 JavaScript 的平台。
- TypeScript与JavaScript的集成
- TypeScript 可以与许多 JavaScript 框架和库无缝集成,如 Angular、React、Vue 等。这使得开发者可以在使用这些框架和库的同时,享受 TypeScript 带来的类型安全和面向对象编程的优势。
- 应用场景与选择
- JavaScript是一种直译式脚本语言,广泛用于前端开发、后端开发和移动端开发等领域。它简单易学、灵活、动态,并且与HTML标识符结合使用,方便用户操作。
- TypeScript则更适合于大型应用或插件的开发,以及需要强类型约束和面向对象编程特性的场景。尽管TypeScript增加了额外的语法和类型检查,可能会增加一些学习成本,但它带来的代码可维护性和可靠性方面的优势,使得它在这些场景中更具优势。
综上所述,TypeScript和JavaScript之间既存在紧密的联系,又各自具有独特的特点和优势。开发者可以根据项目的具体需求和个人的偏好来选择使用哪种语言。
1.2 常用类型
TypeScript 的常用类型包括:
- 基础类型:boolean、string、number、undefined、null、symbol、bigint。
- 复杂类型:array、tuple、enum、object。
- 特殊类型:any、unknown、never、void
举例说明:
复杂类型
1)array 数组:表示元素类型固定的列表。例如:
typescript// 1、在元素类型后面接上[],表示由此类型元素组成的一个数组 let numbers: number[] = [1, 2, 3]; // 2、使用数组泛型,Array<元素类型> let numbers: Array<number> = [1, 2, 3];
2)tuple 元祖:表示已知数量和类型的数组。例如:
typescriptlet x: [string, number] = ["hello", 10]; // 当访问一个已知索引的元素,会得到正确的类型: console.log(x[0].substr(1)); // OK console.log(x[1].substr(1)); // Error, 'number' does not have 'substr' // 当访问一个越界的元素,会使用联合类型替代 x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型 console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString x[6] = true; // Error, 布尔不是(string | number)类型
3)enum 枚举:用于定义一组命名常量。例如:
enum 类型是对 JavaScript 标准数据类型的一个补充。 像 C# 等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。
typescriptenum Color { Red, Green, Blue } let c: Color = Color.Green;
默认情况下,从 0 开始为元素编号。 你也可以手动的指定成员的数值。或者,全部都采用手动赋值。
typescript// 1、手动的指定成员的数值 enum Color { Red = 1, Green, Blue } let c: Color = Color.Green; // 2、全部都采用手动赋值 enum Color { Red = 1, Green = 2, Blue = 4 } let c: Color = Color.Green;
4)object:表示非原始类型的值,例如对象、数组等。例如:
typescriptlet person: { name: string; age: number } = { name: "John", age: 30 };
特殊类型
1)any:表示任意类型,允许任何类型的值。通常用于处理动态内容或逐步迁移到 TypeScript 的项目。例如:
typescriptlet anything: any = "hello"; anything = 10;
2)unknown:表示未知类型,与 any 类似,但更安全,必须在使用之前进行类型检查。例如:
typescriptlet notSure: unknown = 4; if (typeof notSure === "number") { let sure: number = notSure; }
3)never:表示不会发生的值,通常用于标识函数从不会返回(如抛出异常)或永远不会有结果的情况。例如:
typescriptfunction error(message: string): never { throw new Error(message); }
4)void:表示没有返回值的函数。例如:
typescriptfunction warnUser(): void { console.log("This is a warning message"); }
某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void。
高级类型和类型操作
1)联合类型(|)和交叉类型(&): 例如:
typescriptlet id: string | number; let person: Person & Serializable;
2)type 类型别名:用于为类型创建别名。 例如:
typescripttype Point = { x: number; y: number; };
3)interface 接口:用于定义对象的类型。 例如:
typescriptinterface Person { name: string; age: number; } let john: Person = { name: "John", age: 30 };
1.3 对象类型
什么是 TypeScript 的对象类型?
在 TypeScript 中,对象类型用于描述非原始类型的值,比如具有特定结构的对象、数组和函数等。
如何定义对象类型?
我们可以通过 3 种主要方式来定义对象类型:匿名、类型别名、接口。
1)匿名对象。 可以直接用类 JavaScript 的语法定义对象属性,示例如下:
typescriptfunction greet(person: { name: string; age: number }) { return "Hello " + person.name; }
2)类型别名。通过 type 关键字来创建,它为一个特定的对象类型创建了一个新名称。示例如下:
typescripttype Person = { name: string; age: number; }; function greet(person: Person) { return "Hello " + person.name; }
类型别名适用于复杂的类型组合,如联合类型、交叉类型或条件类型。
3)接口。通过 interface 关键字定义,示例如下:
typescriptinterface Person { name: string; age: number; } function greet(person: Person) { return "Hello " + person.name; }
接口与类型别名类似,但接口可以扩展(继承)其他接口:
typescriptinterface Employee extends Person { employeeId: number; } let jane: Employee = { name: "Jane", age: 25, employeeId: 1234 };
接口还可以用于描述函数类型,示例如下:
typescriptinterface SearchFunc { (source: string, subString: string): boolean; } let mySearch: SearchFunc; mySearch = function(source: string, subString: string) { return source.search(subString) !== -1; };
1.4 类型扩展
继承:接口是支持继承的,便于我们扩展对象类型,而且支持多继承,示例代码如下:
typescriptinterface Colorful { color: string; } interface Circle { radius: number; } interface ColorfulCircle extends Colorful, Circle {} const cc: ColorfulCircle = { color: "red", radius: 42, };
交叉类型:除了通过继承实现对象扩展外,TypeScript 还提供了交叉类型,用于组合现有的对象类型。 交叉类型是使用
&
运算符定义的,示例代码如下:typescriptinterface Colorful { color: string; } interface Circle { radius: number; } type ColorfulCircle = Colorful & Circle;
上述代码将 Colorful 和 Circle 相交,生成了一个包含 Colorful 和 Circle 的所有成员的新类型。
1.5 属性修饰符
对象类型中的每个属性都可以指定一些内容:类型、属性是否可选、属性是否可以写入。
可选属性:在对象类型中,我们可以使用
?
来标识可选属性:typescriptinterface Person { name: string; age?: number; // age 是可选的 } let john: Person = { name: "John" };
只读属性:通过
readonly
关键字,可以定义只读属性,防止它们在对象创建后被修改:typescriptinterface Point { readonly x: number; readonly y: number; } let p1: Point = { x: 10, y: 20 }; // p1.x = 5; // 错误,x 是只读属性
索引签名:有时你无法提前知道对象属性的所有名称(key),但你可以明确 key 的类型,就可以使用索引签名。 索引签名允许对象具有未知数量的属性,比如:
typescriptinterface StringArray { [index: number]: string; } let myArray: StringArray = ["Bob", "Fred"]; let first: string = myArray[0]; // Bob
1.6 类型别名
类型别名(Type Alias)是 TypeScript 中用来给某种特定类型创建一个新的名字的功能。它主要用于简化或提高代码的可读性,特别是在处理复杂类型时。定义类型别名使用 type
关键字。
type Name = string; // 为 string 类型创建一个别名叫 Name
type Point = { // 为对象类型创建一个别名叫 Point
x: number;
y: number;
};
type ID = number | string; // 为联合类型创建一个别名叫 ID
type Animal = {
name: string;
age: number;
};
type Cat = Animal & { // 为交叉类型创建一个别名叫 Cat
meow: () => void;
};
与接口的对比
类型别名和接口(Interface)在某些情况下是可以互换使用的,但它们也有区别。比如,接口可以被扩展,而类型别名通常更能适应复杂类型、联合类型和交叉类型。
局限性
类型别名虽然方便,但在一些情况下不如接口灵活。比如,接口可以进行声明合并,而类型别名不行。
typescriptinterface Person { name: string; } interface Person { age: number; } // 结果:{ name: string, age: number } type Person = { name: string; }; type Person = { age: number; }; // 错误:标识符重复
1.7 接口
接口(Interface)是 TypeScript 中用于定义对象结构的工具。它允许我们描述对象的形状,比如对象有哪些属性以及属性的类型。这有助于在代码中提高类型检查的精确度,让代码更为严谨。简单来说,接口就是一种声明,但不会在编译后生成任何代码。
定义接口很简单,使用 interface
关键字来声明接口,然后在大括号 {}
内描述属性和类型。比如:
interface Person {
name: string;
age: number;
greet: () => void;
}
接口的可选属性
有时候,某个属性可能并不是必须的。我们可以使用问号
?
来标记该属性为可选属性:typescriptinterface Person { name: string; age?: number; // 可选属性,可能存在也可能不存在 greet: () => void; }
只读属性
如果你希望某些属性在对象创建后不可更改,可以使用
readonly
修饰符:typescriptinterface Person { readonly id: number; // 只读属性 name: string; greet: () => void; }
readonly vs const: 最简单判断该用 readonly 还是 const 的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const,若做为属性则使用 readonly。
混合类型
接口不仅可以描述对象的属性,也可以描述函数的类型。例如:
typescriptinterface Counter { (start: number): string; // 描述作为函数的类型 interval: number; // 描述作为对象的属性 reset(): void; // 描述作为对象的函数 }
继承接口
接口之间可以互相继承,继承可以帮助我们在大型系统中复用代码:
typescriptinterface Named { name: string; } interface Person extends Named { age: number; greet: () => void; }
1.8 类型别名和接口的区别
TypeScript 的类型别名和接口都有助于定义复杂类型,但它们存在一些关键区别:
用途不同
- 类型别名(Type Aliases)可以用于定义原始类型、联合类型、元组以及复杂对象等各种类型。接口(Interfaces)则主要用于定义对象类型。
扩展方式不同
- 接口可以通过
extends
关键字进行扩展,而类型别名则需要使用交叉类型&
来进行组合。
- 接口可以通过
合并机制不同
- 接口支持声明合并,即可以多次声明同一个接口名称,它们会自动合并。而类型别名不支持这一点,重复声明同名的别名会导致编译错误。
1.9 TypeScript 中的泛型是什么?如何使用泛型?
TypeScript 的泛型是一种让类型可以参数化的工具。它不仅仅局限于某个特定的类型,而是可以接受任意的类型参数。泛型的主要目的是增强代码的复用性,同时保持良好的类型安全性。
TypeScript 还支持泛型对象类型,通过泛型,可以编写能够适用于多种类型的函数、类和接口,而无需在编写代码时指定具体的类型,能够使代码更具通用性和复用性。 常见的使用场景包括泛型接口、泛型类、泛型函数、泛型约束等,示例如下:
泛型接口:泛型接口允许我们定义可以适用于多种类型的接口。例如,定义一个可以操作不同类型数据的容器接口:
typescriptinterface Container<T> { value: T; } let stringContainer: Container<string> = { value: "Hello, TypeScript" }; let numberContainer: Container<number> = { value: 42 };
Array<T>
类型就是一个 TypeScript 内置的泛型接口。泛型类:泛型类与泛型接口类似,允许定义可以操作多种类型数据的类。例如,定义一个泛型栈类:
typescriptclass Stack<T> { private items: T[] = []; push(item: T): void { this.items.push(item); } pop(): T | undefined { return this.items.pop(); } } let stringStack = new Stack<string>(); stringStack.push("Hello"); console.log(stringStack.pop()); // "Hello" let numberStack = new Stack<number>(); numberStack.push(42); console.log(numberStack.pop()); // 42
泛型函数:可以灵活地定义函数的参数和返回值类型,例如,一个返回输入参数的函数:
typescriptfunction identity<T>(arg: T): T { return arg; } let output1 = identity<string>("myString"); let output2 = identity<number>(100);
泛型约束:有时我们希望泛型类型满足某些条件,这时候可以使用泛型约束。例如,定义一个只能操作具有 length 属性的泛型函数:
typescriptinterface Lengthwise { length: number; } function logLength<T extends Lengthwise>(arg: T): T { console.log(arg.length); return arg; } logLength("Hello"); // 输出: 5 logLength([1, 2, 3]); // 输出: 3 logLength({ length: 10, value: 3 }); // 输出:10 // logLength(42); // 错误: number 没有 length 属性
上述代码使用
extends
关键字约束 T 必须满足 Lengthwise 接口,即必须具有 length 属性。
1.10 TypeScript 中的类是什么?如何定义和使用类?
在 TypeScript 中,类(Class)是一种用于创建对象的蓝图或模板。类可以包含属性(成员变量)和方法(成员函数)。通过类,我们可以创建具有相同属性和方法的多个对象(实例)。
定义类
在 TypeScript 中,使用
class
关键字来定义一个类。下面是一个简单的例子:typescriptclass Person { // 属性(成员变量) name: string; age: number; // 构造函数,用于初始化对象时设置属性 constructor(name: string, age: number) { this.name = name; this.age = age; } // 方法(成员函数) greet() { console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`); } }
使用类
要创建一个类的实例(对象),我们使用
new
关键字。下面是如何使用上面定义的Person
类的例子:typescript// 创建 Person 类的实例 const person1 = new Person('Alice', 30); const person2 = new Person('Bob', 25); // 调用实例的方法 person1.greet(); // 输出: Hello, my name is Alice and I am 30 years old. person2.greet(); // 输出: Hello, my name is Bob and I am 25 years old.
访问修饰符
TypeScript 支持三种访问修饰符:
public
、private
和protected
。public
(默认):成员可以在任何地方被访问。private
:成员只能在类内部被访问。protected
:成员只能在类及其子类中被访问。
typescriptclass Person { private name: string; // 私有属性 protected age: number; // 受保护属性 public constructor(name: string, age: number) { this.name = name; this.age = age; } public greet() { console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`); } private sayPrivate() { console.log(`This is a private method. My name is ${this.name}`); } protected sayProtected() { console.log(`This is a protected method. My age is ${this.age}`); } } const person = new Person('Alice', 30); person.greet(); // 可以访问 // person.sayPrivate(); // 错误:'sayPrivate' 是私有的 // person.name; // 错误:'name' 是私有的 class Employee extends Person { department: string; constructor(name: string, age: number, department: string) { super(name, age); this.department = department; } work() { this.sayProtected(); // 可以访问 // this.sayPrivate(); // 错误:'sayPrivate' 是私有的,在子类中也不能访问 } } const employee = new Employee('Bob', 25, 'Engineering'); employee.work(); // 可以调用
静态成员
静态成员属于类本身,而不是类的实例。它们通过类名直接访问,而不是通过实例。
typescriptclass MathUtils { static add(a: number, b: number): number { return a + b; } } const result = MathUtils.add(5, 3); // 调用静态方法 console.log(result); // 输出: 8
抽象类和接口
抽象类:包含抽象方法(没有实现的方法)的类,不能直接实例化,通常用于定义基类。
typescriptabstract class Animal { name: string; constructor(name: string) { this.name = name; } abstract makeSound(): void; // 抽象方法 } class Dog extends Animal { makeSound() { console.log('Woof! Woof!'); } } const dog = new Dog('Rex'); dog.makeSound(); // 输出: Woof! Woof!
接口:用于定义一个类的结构,但不包含实现。一个类可以实现多个接口。
typescriptinterface Greetable { greet(): void; } class Person implements Greetable { name: string; constructor(name: string) { this.name = name; } greet() { console.log(`Hello, my name is ${this.name}`); } } const person = new Person('Alice'); person.greet(); // 输出: Hello, my name is Alice
1.11 TypeScript 中的类有哪些成员可见性?
TypeScript 的类成员有三个主要的可见性修饰符:public、private 和 protected。
- 1)public: 默认的修饰符,表示类成员可以在任何地方访问,没有限制。
- 2)private: 表示类成员只能在声明它的类内部访问,不能在类的实例以及子类中访问。
- 3)protected: 表示类成员可以在声明它的类及其子类中访问,但不能在类的实例中访问。
1.12 TypeScript 中的 static 关键字有什么作用
在 TypeScript 中,static
关键字用于定义类的静态成员。静态成员属于类本身,而不是类的实例,意味着你可以在不实例化类的情况下访问这些成员。静态成员通常用于存储与类本身相关的常量或方法,而不是与某个具体实例相关的内容。
class MathUtils {
static PI: number = 3.14;
static circleArea(radius: number): number {
return this.PI * radius ** 2;
}
}
// 通过类名访问静态成员
console.log(MathUtils.PI); // 输出 3.14
console.log(MathUtils.circleArea(5)); // 输出 78.5
实例成员和静态成员的区别
- 实例成员需要通过类的实例来访问和使用。每个实例都有自己独立的一份实例成员数据。
- 静态成员直接属于类本身,无需创建类的实例。例如,如果你有一个静态方法
ClassName.staticMethod()
,你可以直接用类名来调用它,不需要实例化这个类。 - 静态成员和实例成员之间没有任何关系,即使它们的名字相同。访问静态成员时不要使用
this
关键字,因为this
关键字指向类的实例。
实际应用场景
- 工具类:静态方法通常用于工具类(utility classes),比如数学计算类、时间处理类等。
- 全局状态:某些场景下需要在应用中维护某些全局状态,静态成员可以很方便地实现这一点。
继承中的静态成员
- 静态成员也会被子类继承。如果子类需要访问父类的静态成员,可以直接使用子类的类名或者父类的类名。
- 当然,子类也可以重写静态成员。例子:
typescriptclass Parent { static greet() { return "Hello from Parent"; } } class Child extends Parent { static greet() { return "Hello from Child"; } } console.log(Parent.greet()); // "Hello from Parent" console.log(Child.greet()); // "Hello from Child"
1.13 TypeScript 中的类型注解是什么?如何使用类型注解?
在 TypeScript 中,类型注解(Type Annotations)是一种向编译器提供关于变量、函数参数和函数返回值类型信息的机制。类型注解可以帮助你在开发过程中捕捉潜在的错误,提高代码的可读性和可维护性。
类型注解的基本语法
类型注解的语法非常简单,通常是在变量名或参数名旁边加上冒号(
:
),然后跟上具体的类型。例如:typescriptlet isDone: boolean = false; let age: number = 30; let name: string = "Alice";
使用类型注解的几种常见场景
变量声明
typescriptlet count: number; count = 10; // 正确 count = "ten"; // 错误,TypeScript 会报错
函数参数和返回值
typescriptfunction greet(name: string): string { return `Hello, ${name}!`; } let greeting: string = greet("Bob"); // 正确 // let greetingError: number = greet("Bob"); // 错误,TypeScript 会报错
函数表达式和箭头函数
typescriptconst add = (a: number, b: number): number => a + b; const result: number = add(3, 4); // 正确
数组和元组
typescriptlet numbers: number[] = [1, 2, 3]; // 数组 let tuple: [string, number] = ["foo", 42]; // 元组
对象类型
typescriptlet user: { name: string; age: number } = { name: "Alice", age: 30 };
typescriptinterface Person { name: string; age: number; } let alice: Person = { name: "Alice", age: 30 };
可选的类型注解
TypeScript 是类型推断能力非常强的语言,很多情况下你不需要显式地添加类型注解,编译器会根据上下文自动推断出类型:
typescriptlet inferredNumber = 10; // 编译器推断出 inferredNumber 的类型为 number let inferredString = "Hello"; // 编译器推断出 inferredString 的类型为 string
总结
类型注解是 TypeScript 提供的一个非常强大的特性,它可以帮助你在编译阶段捕捉到类型相关的错误,提高代码的健壮性和可维护性。通过合理使用类型注解,你可以更好地管理项目的类型信息,享受 TypeScript 带来的类型安全优势。
1.14 TypeScript 中的枚举是什么?如何定义和使用枚举?
在 TypeScript 中,枚举(Enum)是一种特殊的数据类型,它允许你定义一组命名的常量。使用枚举可以使代码更加清晰和易于理解,因为它们提供了一组相关的值,并为这些值提供了一个易于使用的命名空间。
定义枚举
你可以使用
enum
关键字来定义一个枚举类型。以下是一个简单的示例:typescriptenum Direction { Up, Down, Left, Right }
在这个例子中,
Direction
是一个枚举类型,它包含四个成员:Up
、Down
、Left
和Right
。默认情况下,枚举成员的值从 0 开始递增,因此Up
的值为 0,Down
的值为 1,依此类推。使用枚举
使用枚举类型非常简单,你只需要像使用其他类型一样使用它即可。以下是一个示例,展示了如何使用
Direction
枚举:typescriptlet direction: Direction = Direction.Up; if (direction === Direction.Up) { console.log('Going up!'); } else if (direction === Direction.Down) { console.log('Going down!'); } else if (direction === Direction.Left) { console.log('Going left!'); } else if (direction === Direction.Right) { console.log('Going right!'); }
枚举的其他特性
手动赋值:你可以为枚举成员手动赋值,后面的成员会按照初始值+1的规则递增(除非你也为它们指定了值)。
typescriptenum ItemStatus { Buy = 1, Send, Receive } console.log(ItemStatus.Buy); // 1 console.log(ItemStatus.Send); // 2 console.log(ItemStatus.Receive); // 3
字符串枚举:从 TypeScript 2.4 开始,你还可以定义字符串枚举。字符串枚举的成员必须是字符串字面量,并且它们不会自动递增。
typescriptenum DirectionString { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT" } console.log(DirectionString.Up); // "UP"
反向映射:枚举还提供了反向映射的功能,即你可以通过枚举值来获取枚举名。这是通过枚举编译后的对象字面量实现的。
typescriptconsole.log(Direction[0]); // "Up" console.log(DirectionString["UP"]); // "Up"
常量枚举:使用
const enum
可以定义一个常量枚举,它在编译时会被完全内联,从而提高性能。但是,请注意,常量枚举不能在运行时通过枚举名来访问枚举成员的值(因为它们被内联了)。typescriptconst enum DirectionConst { Up, Down, Left, Right } let directionConst: DirectionConst = DirectionConst.Up; // 编译后的代码中不会有 DirectionConst 这个枚举类型,只会内联它的值
计算成员和混合枚举:枚举成员还可以是计算值或混合类型(数字和字符串混合),但请尽量避免使用复杂的表达式或混合类型,因为它们可能会使代码变得难以理解和维护。
typescriptenum FileAccess { Read = 1 << 1, Write = 1 << 2, ReadWrite = Read | Write } console.log(FileAccess.Read); // 2 console.log(FileAccess.Write); // 4 console.log(FileAccess.ReadWrite); // 6
总结
枚举是 TypeScript 中一个非常有用的特性,它可以帮助你定义一组相关的常量,并为这些常量提供一个易于理解和使用的命名空间。通过合理使用枚举,你可以使代码更加清晰和易于维护。
1.15 TypeScript 中的命名空间是什么?如何定义和使用命名空间?
在 TypeScript 中,命名空间(Namespace)是一种封装标识符(如变量、函数、类等)的方式,以防止命名冲突。命名空间可以看作是一个内部的 “盒子”,你可以将相关的代码放入这个盒子中,并通过命名空间的名字来访问这些代码。
定义命名空间
你可以使用
namespace
关键字来定义一个命名空间。以下是一个简单的示例:typescriptnamespace Validation { export interface StringValidator { isAcceptable(s: string): boolean; } export class LettersOnlyValidator implements StringValidator { isAcceptable(s: string): boolean { return /^[A-Za-z]+$/.test(s); } } export class ZipCodeValidator implements StringValidator { isAcceptable(s: string): boolean { return /^\d{5}(-\d{4})?$/.test(s); } } }
在这个例子中,我们定义了一个名为
Validation
的命名空间,并在其中定义了一个接口StringValidator
和两个类LettersOnlyValidator
和ZipCodeValidator
。注意,我们使用了export
关键字来导出这些成员,以便它们可以在命名空间的外部被访问。使用命名空间
要访问命名空间中的成员,你需要使用点(
.
)操作符和命名空间的名称。以下是一个示例,展示了如何使用Validation
命名空间中的成员:typescriptlet strings = ["Hello", "98765", "a1b2c3"]; // Validators to use let validators: { [s: string]: Validation.StringValidator; } = {}; validators["ZIP code"] = new Validation.ZipCodeValidator(); validators["Letters only"] = new Validation.LettersOnlyValidator(); // Test each string against the validators for (let s of strings) { for (let name in validators) { let isAcceptable = validators[name].isAcceptable(s); console.log(`'${ s }' ${ isAcceptable ? 'matches' : 'does not match' } '${ name }'.`); } }
在这个例子中,我们创建了一个名为
validators
的对象,其属性是字符串到Validation.StringValidator
类型的映射。然后,我们为每个字符串测试了每个验证器,并输出了结果。嵌套命名空间
命名空间可以嵌套在其他命名空间中,以进一步组织代码。以下是一个示例:
typescriptnamespace Geometry { export namespace Shapes { export class Circle { constructor(public radius: number) {} getArea() { return Math.PI * this.radius ** 2; } } export class Square { constructor(public side: number) {} getArea() { return this.side ** 2; } } } } let myCircle = new Geometry.Shapes.Circle(10); console.log(myCircle.getArea()); // 314.159...
别名与合并命名空间
别名:你可以使用
type
或import
语句为命名空间创建别名,以便更简洁地引用它们。typescripttype GeoShapes = Geometry.Shapes; let mySquare = new GeoShapes.Square(5);
合并命名空间:如果两个命名空间具有相同的名称,它们会被合并成一个命名空间。这允许你将代码拆分到多个文件中,但仍在同一个命名空间中组织它们。
typescript// File: shapes.ts namespace Shapes { export class Triangle { /* ... */ } } // File: geometry.ts namespace Shapes { export class Circle { /* ... */ } } // 合并后的命名空间 // namespace Shapes { // export class Triangle { /* ... */ } // export class Circle { /* ... */ } // }
在上面的例子中,如果
shapes.ts
和geometry.ts
被编译到同一个项目中,那么Shapes
命名空间将包含Triangle
和Circle
两个类。
总的来说,命名空间是 TypeScript 中组织代码、防止命名冲突的强大工具。然而,随着 ES6 模块(使用 import
和 export
)的普及,许多开发者现在更倾向于使用模块来组织代码,因为模块提供了更好的封装和更灵活的代码重用方式。不过,在某些情况下,命名空间仍然是一个有用的特性。
1.16 索引访问类型
TypeScript 的索引访问类型(Indexed Access Types),有时候也被称为查找类型(Lookup Type),是用于获取对象属性类型的一种方式。具体来说,它允许你在已知属性名的前提下,从类型中取出这个属性的类型。
比如说,我们有一个接口 Person
:
interface Person {
name: string;
age: number;
isStudent: boolean;
}
// 我们可以用索引访问类型来获取某一个属性的类型:
type NameType = Person['name']; // NameType 是 string
type AgeType = Person['age']; // AgeType 是 number
索引访问类型是 TypeScript 提供的一种非常强大的类型操作工具,它的应用范围不局限于此。我们可以进一步探讨它在类型安全和灵活性上的其他用法。
可以与联合类型搭配使用:
假设你有一个包含多个属性名的联合类型,你可以用索引访问类型来获取这些属性的类型。
typescripttype PersonKeys = 'name' | 'age'; type PersonValues = Person[PersonKeys]; // PersonValues 是 string | number
可以与条件类型一起使用:
你可以使用条件类型来进行更复杂的类型计算和约束。
typescripttype IsString<T> = T extends string ? true : false; type CheckName = IsString<Person['name']>; // CheckName 是 true type CheckAge = IsString<Person['age']>; // CheckAge 是 false
可以为泛型提供更强的灵活性:
索引访问类型也可以在泛型中使用,使得函数或者组件具有更高的类型精确度。
typescriptfunction getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const person: Person = { name: 'Alice', age: 25, isStudent: true }; const name = getProperty(person, 'name'); // name 的类型是 string const age = getProperty(person, 'age'); // age 的类型是 number
1.17 条件类型
条件类型是 TypeScript 中的一种高级类型工具,它允许我们根据一个条件来选择类型。条件类型的语法类似于 JavaScript 的三元运算符,用来根据某个条件来选择一个类型。其基本的形式是 T extends U ? X : Y
,意思是如果 T 能赋值给 U,那么类型结果为 X,否则为 Y。
基本形式与语法:
T extends U ? X : Y
这个结构中,T
是我们正在检查的类型,U
是目标类型,如果T
能赋值给U
,那么条件为真,类型选择X
,否则选择Y
。
实际应用:
过滤类型:条件类型可以用来过滤一个联合类型的某些类型。例如:
typescripttype Filter<T, U> = T extends U ? never : T; type Result = Filter<string | number | boolean, number>; // Result = string | boolean
在这个例子中,
Filter
类型保证输入类型T
中去掉U
类型,比如Result
中去掉了number
。检查类型:条件类型还可以帮助我们在一些业务逻辑中根据类型选择不同的操作。例如:
typescripttype CheckType<T> = T extends string ? "string type" : "other type"; type Result1 = CheckType<string>; // "string type" type Result2 = CheckType<number>; // "other type"
这样可以有助于我们在泛型编程时,根据不同的类型进行不同的处理逻辑。
分布式条件类型:
一个条件类型如果作用于一个联合类型,会被分配到每个成员。例如:
typescripttype Example<T> = T extends string ? 'yes' : 'no'; type Result = Example<string | number>; // 'yes' | 'no'
在这个例子中,条件类型被分配给
string
和number
后分别得到yes
和no
,最后组合成联合类型yes | no
。
内置条件类型:
- TypeScript 提供了一些内置的条件类型,如
Exclude<T, U>
,Extract<T, U>
,NonNullable<T>
,ReturnType<T>
等,这些都基于条件类型来实现,提供了各种便捷的类型操作工具。
- TypeScript 提供了一些内置的条件类型,如
使用注意事项:
- 虽然条件类型非常强大,但在编写复杂条件时也要注意可读性。类型过于复杂可能会让代码变得难以理解和维护。
避免误区:
- 新手常常搞混条件类型的应用场景和基本语法,例如认为
T extends U
中的extends
是类型继承(其实是类型约束)。这里需要注意明确两者在语义上的不同。
- 新手常常搞混条件类型的应用场景和基本语法,例如认为
1.18 映射类型
TypeScript 的映射类型是一种高级类型,它允许我们根据已有类型生成新的类型。通过使用映射类型,可以将特定的变换应用到类型的属性上,生成一个新的类型。例如,假设我们有一个接口 Person
,其中包含一些属性,现在我们想要创建一个新类型,将 Person
中所有属性都设为可选属性(Partial
类型);或者我们想要创建一个新类型,使 Person
中所有属性变为只读(Readonly
类型)。这些操作都可以通过映射类型来实现。
定义和基本使用:
typescripttype MappedType<T> = { [P in keyof T]: T[P]; }
其中
T
是一个给定的类型,keyof T
会提取 T 中所有的键名,P
则是这些键名,T[P]
表示这些键名所对应的类型。通过这种方式,我们可以遍历 T 中的所有属性,并生成一个新的类型。典型映射类型使用:
Partial:
Partial
将类型中的每个属性变成可选的:typescripttype Partial<T> = { [P in keyof T]?: T[P]; }
Readonly:
Readonly
将类型中的每个属性变为只读:typescripttype Readonly<T> = { [P in keyof T]: Readonly<T[P]>; }
其他实用的映射类型:
Pick:
Pick
类型可以从给定类型中选取一部分属性,构造一个新的类型:typescripttype Pick<T, K extends keyof T> = { [P in K]: T[P]; }
Record:
Record
类型创建一个类型,其属性名是给定的属性名集,属性值是给定的类型:typescripttype Record<K extends keyof any, T> = { [P in K]: T; } type StringRecord = Record<'a' | 'b' | 'c', string>; const stringRecord: StringRecord = { a: 'hello', b: 'world', c: '!' };
1.19 模板字面量类型
TypeScript 的模板字面量类型(Template Literal Types)是一种类型操作,它允许我们通过字符串模板字面量创建新的字符串类型。这是从 TypeScript 4.1 开始引入的特性,允许我们通过组合现有的字符串字面量类型来形成新的、更复杂的字符串类型。
你可以把模板字面量类型看作是 JavaScript 中字符串模板字面量(Template Literals)的类型版本。它能够动态地生成新的类型,基于现有的字符串类型进行匹配和替换。
type Greeting = 'Hello';
type Entity = 'world';
type CompleteGreeting = `${Greeting}, ${Entity}!`; // 'Hello, world!'
在这个例子中,CompleteGreeting
类型将会是字符串字面量类型 'Hello, world!'
。
模板字面量类型的引入大大增强了 TypeScript 处理字符串字面量的能力。接下来可以进一步探讨一下它的能力、使用场景和一些例子:
动态生成类型:
我们可以通过模板字面量类型生成更复杂的数据类型,比如生成 API 的路由、配置等。
typescripttype Route = '/user'; type Action = 'create' | 'update' | 'delete'; type Method = `${Route}/${Action}`; // Method -> '/user/create' | '/user/update' | '/user/delete'
模式匹配和替换:
TypeScript 模板字面量类型同样支持模式匹配(Pattern Matching)和替换。我们可以通过条件类型(Conditional Types)来实现这种效果。
typescripttype Uppercase<S extends string> = S extends `${infer T}${infer U}` ? `${Uppercase<T>}${Uppercase<U>}` : S; type UppercaseHello = Uppercase<'hello'>; // 'HELLO'
组合与类型检查:
利用模板字面量类型,结合泛型和内置的条件类型,我们可以做到更多复杂的类型检查和组合。
typescripttype ExtractRoute<T extends string> = T extends `${infer R}/${infer A}` ? R : never; type RouteUser = ExtractRoute<'/user/create'>; // '/user'
实际应用场景:
- API 字符串路径定义:保证 URL 的一致性和自动化;
- 前端框架中的状态管理、事件处理:定义事件名称、状态属性等的类型,更精准地限制其合法值;
- 国际化处理:使用模板字面量类型对字符串资源进行更严格的类型定义和检查。
2. TypeScript 拓展知识
2.1 使用 TypeScript 进行开发时,有什么心得体会
- 为什么使用 TypeScript?
- TypeScript 是 JavaScript 的超集,它为 JavaScript 添加了 静态类型检查 和更多现代语言特性(比如箭头函数、解构赋值、模块化等),再配合 WebStorm、VSCode 等主流 IDE,可以实现智能提示、代码补全、自动重构等功能,使得代码更加健壮、可维护性更高。
- TypeScript 的常用特性
- 静态类型检查:TypeScript 的核心特性就是静态类型检查,它可以在编译时捕获类型错误,帮助减少运行时错误。通过显式地声明变量、函数参数和返回值的类型,代码更加自文档化且易于维护。
- 类型别名:类型别名允许我们为复杂的类型定义简洁的别名,尤其在处理联合类型或嵌套类型时非常有用。
- 接口:接口用于定义对象的结构和行为,规定了对象应该具备哪些属性和方法。接口还可以用于函数参数的类型检查,确保传入的对象符合预期的结构。
- 联合类型和交叉类型:
- 联合类型:允许变量可以是多种类型中的一种。
- 交叉类型:组合多个类型为一个类型,要求变量满足所有类型的约束。
- 泛型:泛型允许我们编写可以适应多种类型的代码,尤其在定义函数、接口和类时非常有用,可以在不牺牲类型安全性的前提下,让代码更具复用性。
- 类型推断:TypeScript 拥有强大的类型推断能力,能够根据变量的初始值自动推断其类型。
- TypeScript 的具体优势
- 类型安全:TypeScript 引入了静态类型系统,可以在编译阶段捕获许多常见的类型错误,减少运行时错误。例如,传递错误类型的参数或调用不存在的方法等问题可以在开发阶段被发现。
- 更好的代码可维护性:由于有明确的类型定义,TypeScript 代码自带文档性,便于开发者理解、维护和重构代码。随着项目规模的扩大,TypeScript 的类型系统能够帮助团队更好地管理代码。
- 现代语言特性:TypeScript 支持最新的 ECMAScript 特性,如箭头函数、解构赋值、模块化等,并且可以提前使用一些未来的 ECMAScript 特性。同时,TypeScript 还引入了面向对象编程中的类、接口、泛型等特性,增强了 JavaScript 的功能。
- 更强的 IDE 支持:TypeScript 提供了强大的代码编辑器支持,如智能提示、代码补全、类型检查、重构工具等。这些特性可以大大提高开发效率,并减少错误。
- 大型项目更有优势:对于大型项目,TypeScript 提供了模块化和类型检查功能,使得代码的可维护性、可扩展性和团队协作都得到了提升。类型定义可以帮助团队更好地理解和使用代码库。
- 与现有 JavaScript 兼容:TypeScript 是 JavaScript 的超集,可以与现有的 JavaScript 代码无缝集成。你可以在现有项目中逐步引入 TypeScript,而不需要一次性重写所有代码。
- 类型守卫:类型守卫是用于在运行时检查类型并基于检查结果进行类型推断的技术。常见的类型守卫包括
typeof
、instanceof
、自定义类型守卫等。
2.2 void 类型
在 TypeScript 中,void
类型表示没有任何返回值的类型。通常用于函数的返回类型,当函数不返回任何值时,使用 void
进行声明。
function logMessage(message: string): void {
console.log(message);
}
const result = logMessage("Hello, TypeScript!"); // result 的类型是 void
void 与 undefined 的区别
在 TypeScript 中,虽然 void 和 undefined 在某些情况下都代表函数不返回值,但它们是不同的类型:
- void 类型只能表示函数没有返回值。
- undefined 类型可以用来表示变量的值是 undefined。
typescriptlet u: undefined = undefined; let v: void = undefined; function doNothing(): void { return; // 这里隐式返回 undefined } // u = void 0; // 错误: 不能将 void 赋值给 undefined
在回调函数中的使用
void 类型在回调函数中非常常见,尤其是在一些事件处理器或异步操作中,表明回调函数不需要返回值。
typescriptfunction handleEvent(callback: () => void): void { // 执行回调函数 callback(); } handleEvent(() => { console.log("事件已处理"); });
void 在类型声明中的使用
在一些复杂类型声明中,void 也可以用于定义某些操作不应有返回值。例如,在类或接口的方法中。
typescriptinterface Logger { log: (message: string) => void; } class ConsoleLogger implements Logger { log(message: string): void { console.log(message); } }
在泛型中的使用
在泛型类型中,可以将 void 作为占位符,表示一个操作不需要返回值。例如在一些带有回调的 API 中。
typescriptfunction executeOperation<T>(operation: () => T): T | void { try { return operation(); } catch (error) { console.error(error); return; } }
总结来说,void 类型主要用于函数没有返回值的场景,我推荐在所有明确不需要返回值的函数中使用 void 进行标注,以提高代码的可读性和类型安全性。
2.3 any、unkonwn、never
区别
- any 和 unkonwn 在 TypeScript 类型中属于最顶层的 Top Type,即所有的类型都是它俩的子类型。
- 而 never 则相反,它作为 Bottom Type 是所有类型的子类型。
any
TypeScript 的 any 类型是一个特殊的类型,用来表示可以是任何类型的值。当你不确定某个变量会是什么类型,或者你希望类型检查器不对某个变量进行类型检查时,可以使用 any 类型。
使用场景
虽然 any 类型提供了灵活性,但它也会带来一定的风险,使用时应该慎重。通常在以下几种情况下使用 any:
- 从 JavaScript 迁移到 TypeScript 时,逐步进行类型标注。
- 遇到复杂的第三方库类型不明确时。
- 接口接收可能是多种类型的参数时。
类型安全与开发体验
any 类型会使 TypeScript 失去其主要的优势——类型安全。比如,当某个变量被定义为 any 时,编译器不会对它进行类型检查,这可能导致在运行时出现错误。我们通常会尽量避免过度使用 any,以保持良好的代码质量。
替代品
如果你想要兼具灵活性和类型安全,可以考虑使用 unknown 类型。unknown 类型相比 any 更加安全,因为你在使用 unknown 类型时必须进行类型检查,才能执行具体的操作。
typescriptlet value: unknown; value = 42; if (typeof value === 'number') { console.log(value.toFixed()); }
any vs unknown
- any:可以被赋任何值,使用时无任何检查。
- unknown:可以被赋任何值,但在使用时必须做好类型检查。
逐步放宽
TypeScript 提供了一些工具帮助你逐步放宽类型检查,例如类型断言(Type Assertion)和类型守卫(Type Guards),可以在需要时应用更严格的类型检测,以确保代码的安全性。
unkonwn
TypeScript 的 unknown 类型表示我们不知道的值,它和 any 类型都能够表示所有值,但有本质区别。主要区别在于:使用 any 类型时,你可以对该类型的值进行任意操作,而不会有任何类型检查;而使用 unknown 类型时,必须在执行大多数操作前先进行类型检查。
也就是说,unknown 更加安全和严格,它要求你在使用值之前必须明确其类型。
- 为什么使用 unknown
- 用 unknown 代替 any 可以更好地利用 TypeScript 的类型系统,使代码更加健壮。
- unknown 可以帮助我们明确数据流和类型检查,通过强制我们进行类型断言或检查减少错误。
- 使用场景
- 接口的参数或返回值未知时优先使用 unknown,通过运行时的类型检查确保安全。
- 在一些通用函数中,可能需要处理不同类型的数据,通过 unknown 类型可以确保类型安全而不是盲目地使用 any。
- 为什么使用 unknown
never
TypeScript 的 never 类型表示那些永不存在的值。它是 TypeScript 中最严格的类型,通常用于表示函数永远不会返回值(要么抛出异常,要么无限循环)。当你确保某个变量永远不会触发其特定分支时,never 类型就显得尤为有用。
typescriptfunction error(message: string): never { throw new Error(message); } function fail() { return error("Something went wrong!"); } function infiniteLoop(): never { while (true) {} }
使用场景
never 类型常见的使用场景包括以下几个:
- 抛出异常:当函数抛出异常时,它不会返回任何值,因此其返回类型为 never。
- 无限循环:当函数包含无限循环时,它也不会返回,因此返回类型是 never。
- 死代码检测:当 TypeScript 检测到某个代码分支不可能被执行时,它会推断出该分支的类型是 never。
never 和 void 的区别
never 和 void 都表示没有返回值的情况,但它们有本质区别:
- never 类型表示不可能到达的终点,通常用于函数永远不会有返回值的情况。换句话说,函数会执行到抛出异常或者无限循环中。
- void 类型表示没有任何类型,通常用于函数没有返回值的情况。例如,函数可以正常结束但不返回值。
TypeScript 类型系统中的地位
never 是所有类型的子类型,可以赋值给任何类型,但反过来却不行。这个特性使得 never 类型特别适合错误处理和意外情况的处理。
2.4 .d.ts 文件和 @types 的区别
“.d.ts”文件和“@types”在TypeScript中都与类型声明有关,但它们在使用方式和来源上存在显著的区别。
- .d.ts文件
- 定义与功能:“.d.ts”是TypeScript的声明文件,主要用于描述已经存在的JavaScript库或模块的类型。这些文件帮助TypeScript编译器理解JavaScript代码中的类型信息,从而提供更好的类型检查、代码补全和智能提示功能。
- 使用场景:当在TypeScript项目中引入一个没有内置类型声明的JavaScript库或模块时,可能需要手动创建一个“.d.ts”文件来声明该库或模块的类型。此外,对于大型项目,手动创建所有必要的“.d.ts”文件可能是一项繁重的工作,但幸运的是,许多流行的JavaScript库和框架已经提供了这些文件。
- 创建与管理:“.d.ts”文件可以手动创建,也可以使用工具自动生成。在项目中,这些文件通常与TypeScript源文件(.ts文件)一起管理。
- @types
- 定义与功能:“@types”是npm上的一个特殊分支,用于存放TypeScript的类型声明文件。这些文件通常以“@types/库名”的形式命名,并作为npm包发布。它们为TypeScript项目中的JavaScript库提供了类型定义,从而增强了开发体验和代码质量。
- 使用场景:当需要在TypeScript项目中使用一个第三方JavaScript库时,可以通过搜索并安装对应的“@types/库名”包来获得类型支持。这样做的好处是,不需要手动创建类型声明文件,就可以获得类型检查、代码补全等智能提示功能。
- 安装与管理:通过npm安装“@types/库名”包后,TypeScript编译器会自动识别并使用其中的类型声明文件。这些文件通常与实际的JavaScript库分开管理,但可以通过npm方便地安装和更新。
- 区别总结
- 来源:“.d.ts”文件可以是手动创建的,也可以是工具自动生成的,而“@types”则是npm上的一个特殊分支,用于存放由TypeScript社区维护的类型声明文件。
- 使用方式:“.d.ts”文件需要手动引入项目或在项目中创建,而“@types”包则通过npm安装后自动被TypeScript编译器识别和使用。
- 适用场景:“.d.ts”文件适用于需要手动声明类型的场景,而“@types”则适用于需要快速获得第三方库类型支持的场景。
在实际开发中,可以根据具体需求选择合适的方式来提供类型声明,以提高TypeScript项目的类型安全性和开发效率。
2.5 as const 的作用
- 变成字面量类型。
- 将数组内容变成只读。
// 若没有 as const,则会推断为 string[]
// 有 as const,则 color 只能取数组里面的值
const color = ['red', 'blue', 'black'] as const;
2.6 协变和逆变
3. 关键字
3.1 extends
在 TypeScript 中,关键字 extends
主要用于表示类型继承和类型约束。
类型继承:用来实现类的继承。通过
extends
,一个类可以从另一个类继承属性和方法,让代码更加模块化和重用。类型约束:用于泛型约束中,指定泛型参数必须继承某个类型,确保类型安全。
接口继承接口:不仅是类可以继承,接口也可以通过
extends
继承其他接口。这使得接口的定义更加灵活和复用。条件类型:TypeScript 提供了一种条件类型语法,通过
extends
实现条件分支,使得类型定义更有弹性和表达力。typescripttype IsString<T> = T extends string ? 'yes' : 'no'; type T1 = IsString<string>; // 'yes' type T2 = IsString<number>; // 'no'
3.2 infer
TypeScript 中的关键字 infer
通常与条件类型(conditional types)一起使用,用于在类型检查中过度复杂或者未知的类型结构中进行类型推断。它可以从类型中“提取”类型变量,使得我们可以在条件类型的 extends
分支中对其进行操作。这是在处理复杂类型转换和类型推断时非常强大且有用的工具。
简而言之,infer
关键字的主要作用就是“推断类型变量”。
基本用法
- 条件类型通常写作
T extends U ? X : Y
,表示如果类型T
可以赋值给类型U
,则结果类型为X
,否则为Y
。 infer
与条件类型结合使用时,可以用于在extends
条件中进行模式匹配,并且可以提取出类型的一部分进行后续的使用。
typescripttype ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
这里
infer R
意思是,如果T
是一个函数类型,那么从T
中提取其返回值类型R
。如果T
不是函数类型,就返回any
。- 条件类型通常写作
复杂用法
infer
关键字能处理复杂类型结构。比如,我们可以用它来获取数组元素的类型:
typescripttype ElementType<T> = T extends (infer U)[] ? U : T;
在这段代码中,如果
T
是一个数组类型(例如number[]
),那么ElementType<number[]>
推断为number
。递归类型推断
- 在更复杂的泛型编程中,
infer
允许我们对类型进行递归推断。例如,从一个嵌套数组中推断出最内层的元素类型:
typescripttype DeepElementType<T> = T extends (infer U)[] ? DeepElementType<U> : T;
如果我们有类型
number[][]
,DeepElementType<number[][]>
将递归推断结果为number
。- 在更复杂的泛型编程中,
实战应用
- 在实际工作中,经常需要使用 infer 类型进行复杂类型计算,如提取复杂对象中的嵌套类型结构、综合多个条件类型简化复杂类型判断等。例如:
typescripttype FunctionArgumentType<T> = T extends (arg: infer A) => any ? A : never; type ArgumentType = FunctionArgumentType<(x: string) => void>; // ArgumentType 将被推断为 string
类型安全和简化
- 使用 infer 可以提高代码的类型安全(type safety),辅助我们在复杂系统中创建更加细致而准确的类型定义,以及避免显式类型声明所带来的复杂和冗长。
4. 类型操作符
4.1 keyof
keyof 是 TypeScript 中非常重要的类型操作符。它的主要作用是用来获取某个对象类型的所有键(key)并生成一个联合类型。换句话说,keyof 操作符可以帮助我们从对象类型中提取出键的集合。
type Person = {
name: string;
age: number;
location: string;
};
type PersonKeys = keyof Person; // "name" | "age" | "location"
为了更好地理解 keyof 操作符,提供一些实用的扩展点:
类型约束:
keyof 操作符非常适合用来约束一个函数的参数类型,使得函数参数必须是对象类型的某个合法键。这样可以增加代码的类型安全性。
typescriptfunction getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const person: Person = { name: "John", age: 30, location: "NY" }; const name = getProperty(person, "name"); // 正常 // const invalid = getProperty(person, "invalidKey"); // TS 编译错误
结合映射类型:
keyof 操作符还可以和映射类型一起使用,以便创建新的对象类型。在这种情况下,我们可以对某个键集合进行操作并生成新类型。
typescripttype ReadonlyPerson = { readonly [K in keyof Person]: Person[K]; };
结合类型条件:
keyof 还可以和条件类型一起使用,创建更为复杂的类型。比如,我们可以定义一个类型,只包括某个对象类型的字符串键:
typescripttype StringKeys<T> = keyof T extends string ? keyof T : never;
联合类型的拆分:
有时候,keyof 生成的联合类型还可以通过条件类型进一步拆分成单独的类型。
typescripttype UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; type PersonKeysIntersection = UnionToIntersection<keyof Person>; // "name" & "age" & "location"
4.2 typeof
typeof
是 TypeScript 中的一个关键字,用来获取变量或表达式的类型。这在定义复杂类型和进行类型推断时非常有用。它有两种主要用途:
- 1)在运行时,用来获取一个值的类型。
- 2)在静态类型检查时,用来获取一个已有变量的类型,主要用于类型推断。
运行时的
typeof
这个用法跟 JavaScript 中的
typeof
操作符类似,用于在代码运行时返回一个值的数据类型。常见返回值有'number'
,'string'
,'boolean'
,'object'
,'undefined'
,'function'
, 和'symbol'
。typescriptlet num = 42; console.log(typeof num); // 输出 "number"
静态类型检查的
typeof
在 TypeScript 中,
typeof
还可以用于静态类型检查。在这种情况下,它不是在运行时检查值,而是在编译时检查类型。你可以使用typeof
来获取一个变量的类型并将其用于其他地方。typescriptlet num = 42; type NumType = typeof num; // NumType 被推断为 number let anotherNum: NumType; // 这里 anotherNum 的类型就是 number anotherNum = 24; // 这是合法的赋值 // anotherNum = "24" // 这会报错,因为字符串不是 number 类型
扩展:类型推断
typescriptconst person = { name: "John", age: 30 }; type PersonKeys = keyof typeof person; // PersonKeys被推断为 "name" | "age" const key: PersonKeys = "age"; // 合法
扩展:与
keyof
联合使用typescriptconst colors = { red: "red", blue: "blue" }; type ColorKeys = keyof typeof colors; // ColorKeys 被推断为 "red" | "blue" let myColor: ColorKeys = "red"; // 合法赋值 // myColor = "green"; // 非法赋值,因为"green"不是ColorKeys之一
5. 内置工具类型
链接:https://www.mianshiya.com/bank/1810644420521152513?current=2&pageSize=20