前端面试 TypeScript 合集
面试题涉及了 TypeScript 语言的各个方面,包括但不限于基础类型
,变量声明
,函数
,泛型
,类
,模块
,异步编程
,高级类型
,类型注解和类型推断
,ts配置
,声明全局变量、库.d.ts
,装饰器
考点 | 内容 | 示例与应用 |
---|---|---|
类型系统 | 基础类型(string 、number 、boolean )、复杂类型(联合类型、交叉类型、接口、类型别名)、类型推断与守卫 | 设计类型以提高代码安全性与灵活性 |
函数与类 | 函数参数类型、返回值类型、箭头函数、函数重载、类的定义、继承、访问修饰符(public 、protected 、private ) | 提升代码的可读性和扩展性 |
泛型 | 在函数、类和接口中使用泛型实现灵活处理不同类型的输入与输出 | 泛型函数和接口,提升代码的复用性 |
模块化 | ES6 模块语法(import 和 export )、模块解析配置 | 优化模块组织,提升项目的可维护性 |
装饰器 | 类装饰器、方法装饰器、属性装饰器、参数装饰器 | 提升框架应用中的代码功能性和灵活性 |
编译配置 | tsconfig.json 配置选项:编译目标、模块系统、严格模式等 | 提高代码在不同环境中的兼容性和性能 |
工程化实践 | TypeScript 与 JavaScript 的混用、第三方库声明文件(.d.ts 文件)与类型声明 | 支持 TypeScript 项目中的代码集成和库调用 |
通过对以上考点的掌握,可以更好地应对 TypeScript 面试中的问题,并理解其在项目中的实际应用。建议在项目中练习和实践,以提升理解和应用能力。
1. TypeScript 与 JavaScript 的区别?
特性 | TypeScript | JavaScript |
---|---|---|
类型系统 | 静态类型:编译时强类型检查,支持基础类型和复杂类型。 | 动态类型:运行时类型检查,类型不固定。 |
编译过程 | 需要编译成 JavaScript 才能运行。 | 直接在浏览器或 Node.js 中执行,不需要编译。 |
类型注解 | 支持类型注解,允许定义变量、函数参数、返回值的类型。 | 不支持类型注解,所有变量都是动态类型。 |
面向对象编程 | 支持类、接口、继承、访问修饰符等面向对象特性。 | 支持基本的面向对象特性,但不如 TypeScript 强大。 |
泛型 | 支持泛型,允许为函数、类和接口定义通用类型。 | 不支持泛型,所有类型必须在运行时确定。 |
工具支持 | 强大的编辑器支持,代码补全、类型检查、重构等。 | 编辑器支持较少,类型检查和重构工具不如 TypeScript。 |
社区和生态 | TypeScript 社区正在快速增长,许多现代框架都采用 TypeScript(如 Angular、React)。 | JavaScript 拥有庞大的社区和成熟的生态。 |
兼容性 | TypeScript 是 JavaScript 的超集,任何 JavaScript 代码都是合法的 TypeScript 代码。 | JavaScript 代码只能在 JavaScript 环境中运行。 |
2. TypeScript 的优势
- 静态类型检查:在编译时进行类型检查,帮助发现错误,减少运行时错误。
- 增强的编辑器支持:支持代码补全、重构和自动修复,提升开发效率。
- 面向对象编程支持:提供类、接口、继承等面向对象特性,增强代码结构性。
- 提高可维护性:强类型和接口使得代码更具可读性、可维护性和扩展性。
- 广泛的社区支持:TypeScript 越来越多地被现代框架(如 Angular 和 React)和工具(如 VS Code)支持,拥有庞大的开发者社区。
3. JavaScript 的优势
- 广泛应用:JavaScript 是 Web 开发的标准语言,几乎所有的浏览器和服务器都支持。
- 灵活性高:动态类型和松散语法使得代码编写更加灵活,适应快速开发。
- 生态成熟:JavaScript 拥有丰富的第三方库和框架,应用广泛,开发资源充足。
- 无需编译:直接在浏览器中运行,不需要额外的编译步骤。
4. 选择 TypeScript 还是 JavaScript?
选择 TypeScript:
- 项目规模较大,维护性要求高。
- 团队中有多个开发者,类型检查能减少协作时的错误。
- 需要与现有的现代框架(如 Angular、React)集成。
- 对代码质量和可读性要求较高,想要使用静态类型来提升可靠性。
选择 JavaScript:
- 项目较小,开发周期较短,且对类型安全的要求不高。
- 团队成员对 TypeScript 的学习曲线不熟悉。
- 快速开发原型或进行小范围的脚本编写。
通过比较 TypeScript 和 JavaScript 的特性及应用场景,可以根据项目需求和团队技术栈做出最佳选择。
2. 为什么推荐使用 TypeScript?
TypeScript 提供了多个优点,尤其是在大型项目和团队开发中。以下是推荐使用 TypeScript 的几个主要理由:
2.1 提高代码质量和可维护性
- 静态类型检查:TypeScript 提供了强类型检查机制,能在编译阶段捕获潜在的错误。通过强类型,开发者能避免很多由于类型不匹配引发的 bug。
- 类型推断与自动补全:通过类型推断,TypeScript 可以自动推导出变量、函数等的类型,减少手动添加类型的工作量。同时,编辑器中的智能提示、代码补全等功能能有效提升开发效率。
2.2 更好的团队协作
- 明确的接口和类型约定:TypeScript 使得函数、类、模块等的接口更加明确,团队成员之间能够清晰理解数据的结构和预期行为,减少因理解差异而产生的错误。
- 文档化:类型注解本身就是文档的一部分,开发者可以通过查看类型定义快速理解 API 和数据结构,而无需额外的文档。
2.3 现代 JavaScript 特性
- 支持 ES6+ 特性:TypeScript 支持现代 JavaScript 特性,如箭头函数、async/await、类、模块等,还可以将代码编译成兼容不同浏览器的版本,保证跨浏览器兼容性。
- 兼容性:TypeScript 是 JavaScript 的超集,所有有效的 JavaScript 代码都是有效的 TypeScript 代码,现有的 JavaScript 代码可以无缝迁移到 TypeScript 中。
2.4 提升可读性与可扩展性
- 类型定义:TypeScript 强制要求明确声明类型,使得代码更加可读和可维护。团队成员能通过类型注解快速了解一个函数或模块的功能和期望输入输出。
- 面向对象支持:TypeScript 增强了 JavaScript 的面向对象编程支持,提供了类、接口、继承、抽象类等功能,使得大规模的项目开发变得更加模块化和易于管理。
2.5 更好的工具支持
- IDE 支持:TypeScript 提供了与现代 IDE(如 VS Code)集成的强大支持,包括自动补全、代码提示、重构工具等,极大提高了开发效率和代码质量。
- 错误提示与调试:静态类型系统能帮助开发者在编码阶段及时发现潜在问题,避免错误进入生产环境,减少调试时间。
2.6 强大的社区和生态支持
- 流行框架支持:TypeScript 已被主流前端框架(如 Angular、React、Vue)以及许多大型公司(如 Microsoft、Google)广泛使用,且其社区在不断壮大。
- 丰富的类型声明库:TypeScript 通过 DefinitelyTyped 提供了大量的类型声明文件,支持与第三方 JavaScript 库的无缝集成,降低了使用第三方库时的类型错误风险。
2.7 适用于大规模项目
- 可扩展性:TypeScript 提供了更好的组织和结构支持,特别适用于大型项目的开发。通过模块化、类和接口,团队可以更好地分工协作,代码更易于管理。
- 渐进式迁移:对于现有的 JavaScript 项目,TypeScript 可以逐步引入和迁移,无需一次性重写代码。这使得团队可以在不影响现有功能的前提下,逐步享受 TypeScript 的优势。
综上所述,TypeScript 提供了很多 JavaScript 所不具备的优势,尤其在大型项目、团队开发和提高代码质量方面具有明显优势。因此,对于新项目或需要维护的长期项目,推荐使用 TypeScript 来提升开发效率和代码可靠性。
3. 什么是命名空间(Namespace)和模块(Module)
在 TypeScript 中,命名空间和模块都用于组织和管理代码,但它们的工作方式和使用场景有所不同。
特性 | 命名空间(Namespace) | 模块(Module) |
---|---|---|
定义方式 | 使用 namespace 关键字定义 | 使用 export 导出,import 导入 |
作用域 | 内部作用域,通常在单一文件内使用 | 外部作用域,跨文件使用,文件即为模块 |
成员暴露 | 通过 export 显式暴露成员 | 通过 export 暴露成员,通过 import 引入 |
命名冲突问题 | 通过命名空间避免全局命名冲突 | 通过模块化管理,避免全局命名冲突 |
文件结构 | 通常在单一文件中使用 | 支持多个文件和模块,符合文件系统结构 |
按需加载 | 不支持按需加载 | 支持按需加载,避免不必要的资源加载 |
模块化支持 | 较为简单,适合小型项目或脚本 | 支持更强的模块化,适用于大型项目 |
浏览器兼容性 | 需要特殊处理才能在浏览器中运行(通常通过 <script> 标签引入) | ES6 模块可以直接在现代浏览器中运行 |
语法规范 | 不是 ES6 标准,属于 TypeScript 的特有特性 | 遵循 ES6 模块语法,符合现代 JavaScript 规范 |
3.1 命名空间(Namespace)
命名空间(以前称为内部模块)是 TypeScript 中用于组织代码的一种机制。它将相关的变量、函数、类、接口等组织在一起,避免命名冲突。命名空间通过 namespace
关键字定义。
特点:
- 内部作用域:命名空间内部的成员是私有的,外部无法直接访问,除非通过导出(
export
)显式暴露成员。 - 仅适用于同一文件:命名空间通常在单一文件中使用,或者在多个文件中通过
/// <reference path="..."/>
引入,整体结构较为集中。 - 避免命名冲突:通过将相关的代码组织在命名空间中,避免全局作用域中的命名冲突。
示例:
namespace Utils {
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
}
console.log(Utils.add(2, 3)); // 输出 5
使用场景:
- 用于将相关的功能集中在同一个命名空间中,便于组织和管理。
- 适合小型项目或在全局作用域中避免命名冲突。
3.2 模块(Module)
模块是 TypeScript 和 JavaScript 中的代码组织机制,用于将代码拆分成多个文件,每个文件作为独立的模块进行定义。模块通过 export 导出成员,其他模块可以通过 import 引入使用。模块可以是 ES6 模块、CommonJS 模块或 AMD 模块。
特点:
- 外部作用域:模块通过 export 暴露成员,通过 import 导入其他模块的成员。
- 按需加载:每个模块是独立的,按需加载可以提高性能。
- 文件为单位:每个文件就是一个模块,通常与文件系统结构对应。
- 支持现代 JavaScript 规范:支持 ES6 模块语法和动态导入。
// utils.ts
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
// app.ts
import { add, subtract } from "./utils";
console.log(add(2, 3)); // 输出 5
使用场景:
- 用于大型项目中,代码拆分成多个文件和模块,便于管理和维护。
- 支持按需加载和更好的代码模块化,避免将所有代码加载到一个全局作用域中。
4. TypeScript 支持的修饰符总结
1. 访问修饰符
TypeScript 提供了三种主要的访问修饰符,用于控制类成员的可访问性:
修饰符 | 访问范围 | 说明 |
---|---|---|
public | 类内部、外部及子类都可以访问 | 默认访问修饰符,表示成员可以被任意访问。 |
private | 仅类内部可以访问,外部和子类不可访问 | 用于封装类内部实现,限制外部访问和修改。 |
protected | 类内部和子类可以访问,外部不可访问 | 用于允许子类访问父类成员,但限制外部访问。 |
2. 其他修饰符
除了访问修饰符,TypeScript 还支持以下修饰符,用于增强类的功能和行为:
修饰符 | 作用 | 说明 |
---|---|---|
readonly | 声明只读属性,初始化后不可修改 | 属性在初始化时可以赋值,但之后不能修改。 |
static | 声明静态成员,属于类本身而非实例 | 静态成员可以通过类名直接访问,而不需要实例化类。 |
abstract | 声明抽象类或抽象方法,必须由子类实现 | 抽象类不能实例化,抽象方法必须在子类中实现。 |
总结
- 访问修饰符:
public
、private
和protected
用于控制类成员的可访问性,帮助提高封装性。 - 其他修饰符:
readonly
用于声明只读属性,static
用于声明静态成员,abstract
用于声明抽象类和抽象方法,增强了类的灵活性和可扩展性。
这些修饰符帮助开发者更好地控制类的行为和成员的访问,提升了代码的封装性、安全性和可维护性。
5. TypeScript 中有哪些声明变量的方式?
声明方式 | 作用 | 特点 |
---|---|---|
let | 声明可修改的变量 | 块级作用域,可重新赋值 |
const | 声明常量 | 块级作用域,初始化后不能重新赋值(对象引用不可变,属性可变) |
var | 声明变量(函数级作用域) | 存在变量提升,推荐避免使用 |
type 和 interface | 声明自定义类型 | 用于定义复杂类型结构,支持类型注解 |
类型推断 | 自动推断类型 | 根据初始值推断变量类型,减少显式声明类型的需要 |
6. TypeScript 中的 Declare
关键字有什么作用?
在 TypeScript 中,declare
关键字用于声明变量、函数、类等的类型,而无需提供其实现。它主要用于告诉 TypeScript 编译器某些变量、函数或模块的存在,并为其指定类型。这种方式通常用于引入外部库或声明全局变量,尤其在没有类型定义文件(如 .d.ts
文件)时。
作用 | 描述 |
---|---|
声明全局变量 | 用于声明在全局作用域中存在的变量,而不提供具体实现 |
声明外部模块 | 用于声明外部模块,通常结合 module 使用,提供模块的类型定义 |
声明函数和类 | 用于声明函数或类的类型定义,而不提供实现 |
声明类型别名 | 用于声明类型别名,简化复杂类型的使用 |
1. 声明全局变量或模块
使用 declare
可以告诉 TypeScript 某个变量或模块的类型,而不需要提供实际的实现。例如,在通过外部 <script>
标签引入的第三方库中,可以使用 declare
来声明这些库的类型。
declare var jQuery: any;
2. 声明外部模块
declare 也用于声明外部模块或包,以便 TypeScript 知道如何引用这些模块。
declare module "myLibrary" {
export function doSomething(): void;
}
上面的代码声明了一个名为 myLibrary 的外部模块,并为其提供了 doSomething 方法的类型定义。
3. 声明函数和类
declare 还可以用于声明外部函数和类。通常用于第三方库或其他不在当前代码文件中的代码。
declare function myFunction(x: number): number;
declare class MyClass {
constructor(name: string);
greet(): void;
}
4. 声明类型(例如类型别名)
通过 declare 可以声明一个类型别名,而不必在代码中提供具体实现。
declare type MyType = string | number;
7. 枚举(enum)是什么,它的优势,应用案例。枚举和常量枚举的区别
在 TypeScript 中,枚举(enum) 是一种用于定义一组命名常量的方式。它可以为一组相关的数字或字符串值指定有意义的名字,帮助我们更好地组织和管理常量值。
枚举的类型:
- 数字枚举:每个枚举成员都被赋予一个数字值,默认从 0 开始递增。
- 字符串枚举:每个枚举成员都被赋予一个字符串值。
- 异构枚举:既包含数字成员,也包含字符串成员。
2. 枚举的优势
- 可读性:通过为常量值赋予有意义的名字,代码更加易读和易维护。
- 类型安全:枚举成员是具有特定类型的常量,使用时 TypeScript 会进行类型检查。
- 代码自描述性:枚举让常量的值更具描述性,可以帮助开发者理解其目的。
- 反向映射:数字枚举支持反向映射,允许你通过枚举值找到对应的枚举名称。
特性 | 普通枚举(enum ) | 常量枚举(const enum ) |
---|---|---|
运行时行为 | 生成一个对象,包含枚举的值与名称 | 编译时内联,不生成运行时对象 |
性能 | 存在运行时开销 | 没有运行时开销,性能更高 |
使用场景 | 适用于需要在运行时访问枚举成员的情况 | 适用于只在编译时需要的常量值,性能要求高时 |
总结:
- 枚举 使得代码中的常量更具可读性和类型安全,尤其适用于需要一组相关常量的场景。
- 常量枚举 提供了更高的性能,通过编译时的内联替换减少了运行时的开销,适合常量值的场景。
8. 类型声明和类型推断的区别,并举例应用
特性 | 类型声明(Type Declaration) | 类型推断(Type Inference) |
---|---|---|
定义方式 | 明确指定类型 | 编译器自动推断类型 |
代码冗长度 | 需要手动声明类型 | 不需要声明,减少代码量 |
类型明确性 | 类型固定,无法改变 | 类型由编译器推断,可能更灵活 |
灵活性 | 类型固定,适合严格类型控制的场景 | 类型推断使得开发过程更为简洁 |
类型推断
let count = 10;
count = 20; // 正确
count = "Hello"; // 错误,推断出的类型是 number
类型声明
let count: number = 10;
count = 20; // 正确
count = "Hello"; // 错误,类型不匹配
9. 什么是接口(interface),它的作用,接口的使用场景。接口和类型别名(Type Alias)的区别
1. 什么是接口(interface)?
在 TypeScript 中,接口(interface) 是一种用于定义对象类型的结构化方式。它用于描述对象的形状(即它有哪些属性和方法),并约束对象必须遵循的结构。接口可以帮助开发者在代码中强制执行特定的结构和行为。
2. 接口的作用
- 类型约束:接口可以强制对象遵循特定的结构,提供类型安全。
- 代码自描述:接口使得对象结构更加清晰,易于理解和维护。
- 扩展性:接口支持继承,可以通过
extends
关键字创建新接口,扩展已有接口的功能。 - 代码重用:可以在多个类或对象中复用接口,减少代码冗余。
3. 接口的使用场景
描述对象的结构:接口最常见的用途是描述对象的形状和结构,确保对象具有特定的属性和方法。
函数类型定义:可以用接口来定义函数的类型,确保函数的参数和返回值符合预期。
类的结构约束:接口可以作为类的契约,确保类具有特定的属性和方法。
4. 接口和类型别名(Type Alias)的区别
特性 | 接口(interface ) | 类型别名(type ) |
---|---|---|
定义类型 | 用于定义对象、类、函数的结构 | 用于定义任意类型,包括对象、联合类型、交叉类型等 |
扩展性 | 支持继承和扩展(extends ) | 通过交叉类型实现扩展,但不直接支持继承 |
声明合并 | 可以多次声明同名接口,TypeScript 会自动合并 | 不能多次声明同名类型别名 |
灵活性 | 适合描述具有结构的类型(如对象、类等) | 更适合复杂的类型,如联合类型、交叉类型等 |
使用场景 | 主要用于描述对象、类的形状和方法,适用于面向对象编程 | 适用于更加灵活、复杂的类型定义 |
5. 总结
- 接口:主要用于描述对象、类、函数的结构,适用于结构化类型的定义,支持继承和合并。
- 类型别名:用于定义任意类型,包括基本类型、联合类型、交叉类型等,灵活性更高,但不支持声明合并。
10. 什么是联合类型和交叉类型
1. 联合类型(Union Type)
联合类型(Union Type) 允许一个变量或参数的类型是多个类型之一。在 TypeScript 中,可以通过使用竖线 (|
) 将多个类型组合起来,表示变量可以是其中的任何一个类型。
语法:
let value: type1 | type2;
2. 交叉类型(Intersection Type)
交叉类型(Intersection Type) 用于将多个类型合并成一个类型,它表示该类型同时具有多个类型的特征。交叉类型通过 & 符号来组合多个类型,表示一个变量必须同时满足多个类型的约束。
语法:
let value: type1 & type2;
特性 | 联合类型(` | `) | 交叉类型(& ) |
---|---|---|---|
作用 | 表示一个变量可以是多个类型之一 | 表示一个变量必须是多个类型的结合体 | |
结果 | 结果类型是多个类型之一,取其中之一 | 结果类型是所有类型的合并,必须满足所有类型的要求 | |
适用场景 | 用于表示一个变量可能是多个类型中的一种 | 用于表示一个变量需要同时满足多个类型的特征 | |
类型检查 | 在多个类型中取其一,允许某些类型缺失 | 所有类型的特征都必须存在,类型更加严格 |
11. 什么是 TypeScript 中的声明文件(Declaration Files)
1. 声明文件(Declaration Files)定义
声明文件(Declaration Files) 是一种特殊的文件类型,用于为 JavaScript 代码提供类型信息。声明文件的扩展名为 .d.ts
,它们通过定义类型、接口、函数等,为 TypeScript 提供类型支持,使 TypeScript 能够理解 JavaScript 中没有类型注解的部分。声明文件并不包含实际的代码实现,它们仅仅是为了声明变量、函数、类等的类型。
2. 声明文件的作用
- 类型定义:声明文件可以为外部 JavaScript 库、第三方包、全局变量等提供类型信息。
- 增强代码补全和类型检查:通过声明文件,TypeScript 可以正确地理解和检查 JavaScript 代码,提供类型安全性。
- 兼容性:声明文件可以让 TypeScript 支持原本没有类型定义的 JavaScript 库或代码,增强语言的兼容性。
3. 使用声明文件的场景
为第三方 JavaScript 库提供类型定义:例如,当你使用一个没有自带 TypeScript 类型定义的 JavaScript 库时,你可以通过声明文件为其添加类型支持。
例如,使用
lodash
库时,lodash
会有一个声明文件提供类型支持。为全局变量声明类型:声明文件可以用来为项目中的全局变量或全局函数声明类型,以便 TypeScript 能够正确识别。
自定义模块的类型声明:如果你自己编写的 JavaScript 模块没有类型信息,你可以通过
.d.ts
文件为其提供类型声明。
4. 声明文件的基本结构
声明文件的常见结构包括以下几种类型:
为变量或常量声明类型
为函数声明类型
为类声明类型
为模块声明类型
typescriptdeclare const someVar: string; declare function greet(name: string): void; declare class Person { name: string; constructor(name: string); } declare module "my-module" { export function doSomething(): void; }
5. 声明文件的引用方式
在 TypeScript 中,可以通过以下方式引用声明文件:
- 通过
/// <reference>
引用声明文件:可以在需要引用的文件中显式指定声明文件的路径。
/// <reference path="path/to/declaration.d.ts" />
- 通过
typeRoots
或types
配置引入:在tsconfig.json
中配置,使得 TypeScript 自动找到声明文件。
{
"compilerOptions": {
"typeRoots": ["node_modules/@types", "path/to/custom/types"]
}
}
6. 总结
- 声明文件:为 JavaScript 代码提供类型信息,使 TypeScript 能够理解 JavaScript 中没有类型注解的部分。
- 作用:声明文件可以帮助 TypeScript 提供类型安全性、增强代码补全、为第三方库、全局变量提供类型支持等。
- 使用场景:用于第三方库、全局变量、自己写的 JavaScript 模块等的类型声明。
12. 什么是类型断言(Type Assertion)
1. 类型断言的定义
类型断言(Type Assertion) 是一种手动指定变量类型的方式,告诉 TypeScript 编译器相信你所声明的类型。通过类型断言,开发者可以绕过 TypeScript 的类型推断,明确地指定变量的类型。类型断言并不会改变变量的实际类型,它只是编译时的一种告诉编译器如何对待该类型的方式。
2. 类型断言的作用
- 提高类型推断的灵活性:有时候 TypeScript 无法根据上下文推断出正确的类型,类型断言可以让你明确指定类型。
- 避免类型错误:在某些情况下,你可以明确知道某个值的类型,但 TypeScript 的推断类型可能不准确。类型断言帮助你避免不必要的类型错误。
- 增强开发体验:通过类型断言,可以使代码在类型上更加精确,提升代码的可读性和可维护性。
3. 类型断言的语法
TypeScript 提供了两种常见的类型断言语法:
- 尖括号语法:typescript
let value = <string>someValue;
- as 语法:typescript
let value = someValue as string;
13. 类型守卫(Type Guards)
1. 类型守卫的定义
类型守卫(Type Guards) 是 TypeScript 中用于细化类型的一种机制。通过在运行时对变量进行类型检查,类型守卫可以帮助编译器在特定的代码块中更精确地推断变量的类型,从而提高类型安全性。
2. 类型守卫的作用
- 类型细化:允许开发者细化变量的类型,让 TypeScript 能够在特定的代码块中进行更精确的类型推断。
- 提升类型安全:在条件分支中,类型守卫确保了变量的类型符合预期,从而减少潜在的类型错误。
- 改善代码可读性:通过显式的类型检查,类型守卫使代码更易于理解和维护。
3. 常见的类型守卫
typeof
类型守卫:用于检查变量是否属于某些基本类型(如string
、number
、boolean
等)。
function example(x: number | string) {
if (typeof x === "string") {
// 在此分支中,x 被推断为 string 类型
console.log(x.toUpperCase());
} else {
// 在此分支中,x 被推断为 number 类型
console.log(x.toFixed(2));
}
}
instanceof
类型守卫:用于检查一个对象是否是某个类的实例,通常用于类和构造函数的类型检查。
class Dog {
bark() {
console.log("Woof!");
}
}
function speak(animal: Dog | string) {
if (animal instanceof Dog) {
// 在此分支中,animal 被推断为 Dog 类型
animal.bark();
} else {
// 在此分支中,animal 被推断为 string 类型
console.log(animal.toUpperCase());
}
}
- 自定义类型守卫:通过自定义函数来实现类型守卫,通常通过返回一个布尔值并使用
is
关键字来告诉 TypeScript 类型。
class Dog {
interface Fish {
swim: () => void;
}
interface Bird {
fly: () => void;
}
function isFish(animal: Fish | Bird): animal is Fish {
return (animal as Fish).swim !== undefined;
}
function move(animal: Fish | Bird) {
if (isFish(animal)) {
animal.swim(); // animal 被推断为 Fish 类型
} else {
animal.fly(); // animal 被推断为 Bird 类型
}
}
4. 类型守卫与类型推断
类型守卫结合 TypeScript 的类型推断功能,可以更准确地推断变量的类型。通过使用类型守卫,TypeScript 可以在条件语句中对变量进行类型细化,减少类型不匹配的风险,提升代码的类型安全性。
5. 总结
- 类型守卫:通过运行时检查变量类型,细化和确保类型安全。
- 作用:增强类型推断的准确性,提升代码的安全性和可读性。
- 常见类型守卫:
typeof
:用于基本类型检查。instanceof
:用于类实例类型检查。- 自定义类型守卫:通过函数细化类型。
14. 索引类型(Index Types)
// 示例 1: 字符串索引类型
interface StringIndexedObject {
[key: string]: number;
}
const obj: StringIndexedObject = {
apples: 5,
oranges: 10
};
console.log(obj["apples"]); // 5
console.log(obj["oranges"]); // 10
// 示例 2: 数字索引类型
interface NumberIndexedObject {
[index: number]: string;
}
const arr: NumberIndexedObject = ["apple", "banana", "cherry"];
console.log(arr[0]); // "apple"
console.log(arr[1]); // "banana"
// 示例 3: 索引类型与映射类型的结合
type Person = {
name: string;
age: number;
};
type PersonProperties = {
[K in keyof Person]: string; // 将所有 Person 类型的属性变为 string 类型
};
const person: PersonProperties = {
name: "John",
age: "30" // 注意这里,age 被强制转换成了字符串类型
};
console.log(person.name); // "John"
console.log(person.age); // "30"
1. 索引类型的定义
索引类型(Index Types) 是 TypeScript 中的一种类型机制,允许通过动态索引来访问对象的属性类型。通过使用索引类型,可以根据一个已知的键(通常是字符串或数字类型)来访问对象的某个属性类型。它是对象类型的一种扩展,支持通过索引来获取对象中某些属性的类型。
2. 索引类型的作用
- 动态访问类型:允许在编译时不确定的情况下,根据索引来动态地访问对象属性的类型。
- 增强灵活性:通过索引类型,可以更加灵活地处理对象属性,尤其是当属性名称不固定时。
- 类型安全:即使是动态访问属性,TypeScript 仍然能确保类型安全,避免不匹配的类型操作。
3. 索引类型的语法
索引类型使用 [key: K]
的形式来声明,其中 K
是可以作为索引的类型,通常为 string
或 number
。[key: K]: T
的意思是对象的属性 key
的类型是 T
。
4. 常见的索引类型
字符串索引类型:使用字符串作为索引的类型。
数字索引类型:使用数字作为索引的类型,通常用于数组类型或其他基于数字索引的集合。
类型声明可以为对象指定属性的类型,并通过索引类型保证对属性的访问是安全的。
5. 索引类型和映射类型的结合
索引类型和映射类型(Mapped Types)可以结合使用,以便动态生成类型。这允许你根据现有的类型进行转换、扩展或者修改。
6. 索引类型的使用场景
- 动态属性访问:当对象的属性名称不固定时,可以使用索引类型来动态访问对象的属性类型。
- 数组和对象的类型约束:使用索引类型为数组或对象的元素类型定义约束,确保访问时类型的一致性。
- 与映射类型配合使用:通过映射类型结合索引类型,可以生成新的类型,动态地调整对象的结构或类型。
7. 总结
- 索引类型:允许通过指定索引的类型来访问对象中属性的类型,为动态访问提供类型安全。
- 作用:增强代码的灵活性,同时保证类型安全。
- 常见类型:字符串索引类型和数字索引类型。
- 应用场景:动态属性访问、类型约束、与映射类型结合使用等。
15. TypeScript 数据类型
1. 基本数据类型
TypeScript 提供了一些基本的数据类型,用于确保代码在编译时能够进行类型检查,增强类型安全性:
number
:表示所有数字类型,包括整数、浮动小数、NaN
和Infinity
。string
:表示文本数据,使用单引号、双引号或反引号(模板字符串)括起来。boolean
:表示布尔值,只有true
和false
两个值。void
:表示没有返回值,通常用于函数返回类型。null
和undefined
:分别表示空值和未定义的值。在strictNullChecks
模式下,它们不能赋给其他类型。any
:允许赋值为任何类型,绕过 TypeScript 的类型检查,适用于不确定类型的场景。never
:表示永远不会有值的类型,通常用于无法返回的函数。
2. 对象类型
object
:表示非原始类型的集合,包括函数、数组、对象字面量等。
3. 数组类型
可以使用两种方式声明数组类型:
- 数组类型:使用
T[]
来表示元素类型为T
的数组。 - 泛型类型:使用
Array<T>
来表示元素类型为T
的数组。
4. 元组类型
元组(Tuple) 是一种数组类型,其中元素的数量和类型是固定的。它允许包含不同类型的元素。
5. 枚举类型
枚举(enum) 是 TypeScript 中的一种数据类型,用来表示一组固定的常量值。可以是数字值或字符串值。
- 数字枚举:默认从
0
开始递增。 - 字符串枚举:使用字符串作为枚举值。
6. 联合类型(Union Types)
联合类型 允许一个变量的类型是多个类型中的任意一个,使用 |
运算符来表示。
7. 字面量类型
字面量类型 用于限制变量可以取的具体值,常用于字符串、数字或布尔值的具体值。
8. 类型别名(Type Alias)
类型别名 用来给一个类型定义一个新的名字,通常用于复杂类型的简化。
9. 函数类型
函数类型 用于定义函数的输入和输出类型。可以通过函数声明、箭头函数等方式定义函数类型。
10. 类型推断
TypeScript 会根据变量的赋值推断出其类型,这被称为 类型推断。大多数情况下,TypeScript 能自动推断出变量的类型,无需显式声明。
11. 其他高级类型
- 交叉类型(Intersection Types):使用
&
运算符组合多个类型,表示同时具备多个类型。 - 映射类型(Mapped Types):通过对现有类型进行映射,生成新的类型。
- 条件类型(Conditional Types):基于条件选择类型的一种方式。
12. 总结
- TypeScript 提供了丰富的数据类型,包括基本数据类型、数组、元组、枚举、联合类型等。
- 通过类型别名、字面量类型、函数类型等可以定义复杂类型。
- TypeScript 的类型推断功能能帮助开发者减少显式类型声明,提高代码的可读性和开发效率。
16. TypeScript 中的静态类型和动态类型有什么区别
1. 静态类型
静态类型 指的是在编译时确定类型。在静态类型的语言中,所有变量的类型在代码编译时就已经确定,并且在编译过程中会进行类型检查。如果变量的类型与期望的类型不匹配,编译器会报错。
特点:
- 类型在编译时就确定。
- 编译器在编译阶段进行类型检查,确保类型安全。
- 通过类型推断、显式声明或接口来定义变量的类型。
- 有助于减少运行时错误,提供更高的代码可靠性。
在 TypeScript 中的体现:
- TypeScript 是一种静态类型语言,尽管它允许动态类型,但主要依赖类型声明和推断来确保类型安全。
- TypeScript 强制要求在编译时进行类型检查,以减少类型错误的可能。
2. 动态类型
动态类型 指的是在运行时确定类型。在动态类型的语言中,变量的类型可以在运行时发生变化,编译器不会对类型进行严格检查,类型的错误通常在运行时才会被发现。
特点:
- 类型在运行时才确定。
- 变量的类型可以在程序执行过程中随时更改。
- 运行时错误可能更难追踪,因为类型错误可能直到运行时才出现。
- 编译器对类型的检查较少或没有检查。
在 JavaScript 中的体现:
- JavaScript 是一种动态类型语言,变量的类型是在运行时确定的,可以随时更改类型。
- JavaScript 不进行类型检查,类型错误通常是在代码执行时才发现的。
3. 静态类型与动态类型的比较
特性 | 静态类型 | 动态类型 |
---|---|---|
类型检查时机 | 编译时 | 运行时 |
类型确定方式 | 编译时确定类型 | 运行时确定类型 |
错误发现 | 在编译时发现类型错误 | 在运行时发现类型错误 |
语言示例 | TypeScript、Java、C++、C# 等 | JavaScript、Python、Ruby 等 |
优点 | 早期发现错误,增强代码可靠性和可维护性 | 灵活性高,开发更快速,适合快速迭代开发 |
缺点 | 需要更多的类型声明,可能增加代码复杂度 | 可能导致运行时错误,缺少编译时的类型检查 |
4. 总结
- 静态类型 在编译时检查类型,能够提前发现类型错误,提高代码的可维护性和稳定性。TypeScript 就是静态类型语言的一个典型例子。
- 动态类型 变量类型在运行时才确定,提供了更大的灵活性,但也可能增加运行时错误的风险,适用于快速开发和原型设计。
TypeScript 是通过结合静态类型和动态类型的特性,既能保证类型安全,又能提高开发效率。它的静态类型系统在编译时提供严格的类型检查,而在运行时仍然支持动态行为。
17. 如何约束泛型参数的类型范围
在 TypeScript 中,泛型提供了类型的灵活性,但有时我们希望限制泛型参数的类型范围。为了做到这一点,可以使用 类型约束。通过约束泛型类型参数,可以确保泛型参数满足特定的类型要求。
// 定义一个接口 Animal
interface Animal {
name: string;
}
// 定义一个函数 greet,泛型 T 被约束为 Animal 类型或其子类型
function greet<T extends Animal>(animal: T): void {
console.log(`Hello, ${animal.name}`);
}
// 正确的调用,传入符合 Animal 类型的对象
greet({ name: "Dog" }); // 输出: Hello, Dog
// 错误的调用,传入不符合 Animal 类型的对象
// greet({ age: 5 }); // 编译错误: 类型 '{ age: number; }' 不满足 'Animal' 类型的约束
1. 使用 extends
约束泛型参数
通过 extends
关键字,可以为泛型类型参数指定一个父类型或接口,这样该泛型类型就会被限制为该父类型或接口及其子类型。
- 通过
T extends U
,可以确保泛型类型T
是U
类型或其子类型。这样就限制了T
的可选范围。
2. 常见的约束方式
约束为某个接口或类型:可以将泛型限制为某个接口或类型,确保传入的泛型参数符合该类型的要求。
约束为基本数据类型:可以约束泛型为某些基本数据类型,如
number
、string
等,以限制传入的类型。约束为类的实例:泛型还可以限制为某个类的实例类型,确保传入的泛型类型是该类的一个实例。
结合多个约束:可以使用交叉类型(
&
)来结合多个约束,使泛型类型同时满足多个类型的要求。
3. 总结
- 类型约束 是通过
extends
关键字实现的,确保泛型类型参数满足特定类型或接口的要求。 - 约束泛型可以增强代码的类型安全,避免传入不符合要求的类型。
- TypeScript 提供了灵活的方式来约束泛型,确保函数或类能够处理预期的类型范围。
18. 什么是泛型约束中的 keyof
关键字,举例说明其用法
在 TypeScript 中,keyof
是一种用于获取对象类型的所有键的类型的操作符。当它与泛型结合使用时,可以对泛型参数进行更精确的约束,确保传入的泛型类型符合特定的键名要求。
1. keyof
的基本概念
keyof
用于获取一个对象类型的所有键(属性名)的联合类型。也就是说,keyof T
会返回类型 T
所有属性名称的联合类型。
例如,给定一个类型 Person
,keyof Person
会返回 "name" | "age"
这样的字符串联合类型。
2. keyof
在泛型约束中的应用
在泛型约束中,keyof
可以确保传入的泛型类型的键名符合某个特定的类型。它通常与 extends
结合使用,用于约束泛型参数只接受某个类型的属性名。
3. 示例代码
interface Person {
name: string;
age: number;
address: string;
}
// 使用 keyof 作为泛型约束,确保传入的类型是 Person 的键名之一
function getProperty<T extends Person, K extends keyof T>(
obj: T,
key: K
): T[K] {
return obj[key];
}
// 正确的调用,key 是 Person 类型的有效属性名
const person: Person = { name: "John", age: 30, address: "123 Main St" };
const name = getProperty(person, "name"); // 返回 'John'
// 错误的调用,key 必须是 Person 类型的属性名
// const invalid = getProperty(person, "nonexistent"); // 编译错误: 'nonexistent' 不是 'name' | 'age' | 'address' 的类型
19. 什么是装饰器,有什么作用,如何在 TypeScript 中使用类装饰器
1. 装饰器简介
装饰器 是一种特殊的声明,能够修改类、方法、访问符、属性或参数的行为。装饰器的概念来源于 元编程,允许在代码运行时,动态地修改类的元数据或行为。
在 TypeScript 中,装饰器是通过 @
符号进行声明的,可以应用于类、方法、属性、方法参数等。装饰器本质上是一个函数,它会接受被装饰的目标(类、方法等)作为参数,并返回一个新的功能或行为。
2. 装饰器的作用
装饰器可以用于多种场景,主要用途包括:
- 增强功能:给类、方法或属性添加额外的功能,例如日志记录、性能测量等。
- 修改元数据:修改类的元数据或描述信息,用于框架和库中,像 Angular 就大量使用装饰器来定义组件、服务等。
- 代码优化:通过装饰器动态增强类或方法的功能,避免修改现有代码结构。
3. 类装饰器
类装饰器 是装饰器的一种类型,用于修改类的构造函数。类装饰器是应用于类构造函数的函数,在类实例化之前执行。
类装饰器的基本语法
function MyDecorator(constructor: Function) {
// 装饰器逻辑
console.log("Class decorated:", constructor.name);
}
@MyDecorator
class MyClass {
constructor() {
console.log("MyClass instance created");
}
}
- MyDecorator 是装饰器,它接收一个构造函数作为参数,可以对类进行修改。
- @MyDecorator 语法应用于 MyClass 类,表示 MyClass 类被装饰器 MyDecorator 装饰。
类装饰器的使用场景
- 添加静态方法:你可以通过装饰器向类动态添加静态方法或属性。
- 修改构造函数:装饰器可以修改类的构造函数,创建类实例时注入额外的功能。
4. 装饰器的使用要求
要使用装饰器,必须启用 TypeScript 的实验性装饰器功能。在tsconfig.json
中添加如下配置:
{
"experimentalDecorators": true
}
5. 总结
- 装饰器 允许在不修改现有代码结构的情况下,动态地增强类、方法、属性等的功能。
- 类装饰器 是专门用来修改类构造函数的装饰器,通常用于增强类功能、添加元数据等。
- 在 TypeScript 中,要使用装饰器,必须启用 experimentalDecorators 配置选项。
通过装饰器,TypeScript 提供了强大的元编程能力,尤其在开发大型应用时,能够减少代码重复和提高灵活性。
20. 类装饰器和方法装饰器的执行顺序是怎样的?
21. TypeScript 中 never 和 void 的区别?
特性 | void | never |
---|---|---|
表示的含义 | 函数没有返回值 | 函数不会返回任何值,通常是抛出异常或进入无限循环 |
常见用途 | 用于标注函数没有返回值 | 用于标注无法完成的函数,如抛出异常或无限循环 |
是否可以返回值 | 可以返回 undefined (默认返回值) | 永远不会返回任何值 |
类型推断 | 如果函数没有显式返回值,会推断为 void 类型 | never 类型用于描述不可能的返回状态 |
22. TS 中 any
和 unknown
有什么区别?
特性 | any | unknown |
---|---|---|
类型安全 | 不进行类型检查,可以赋值为任何类型 | 需要进行类型检查才能使用,不允许直接赋值或操作 |
赋值操作 | 可以将 any 类型赋值给任何类型 | 不能将 unknown 类型赋值给其他类型,必须先进行类型检查 |
类型推断 | 类型推断为 any 时,所有的类型检查会被跳过 | 类型推断为 unknown 时,必须先进行类型检查后才能操作 |
安全性 | 提供极低的类型安全,容易引发运行时错误 | 提供较高的类型安全,确保在使用前进行类型确认 |
使用场景 | 用于不关心类型,或临时跳过类型检查的场景 | 用于希望保持类型安全的情况下,要求显式进行类型检查 |
类型的宽松程度 | 最宽松的类型,适用于非常动态的代码需求 | 比 any 更严格,要求明确类型才能操作 |
23. TS 中什么是方法重载?
1. 方法重载的定义
方法重载 是指在同一个作用域内,多个函数可以具有相同的名称,但是它们的参数类型或返回值类型不同。通过方法重载,TypeScript 可以根据传入的参数不同来调用不同的函数实现。
在 TypeScript 中,方法重载并不是通过多次定义同名函数来实现的,而是通过一组不同参数签名来声明多个方法,然后在实现部分提供一个匹配所有签名的函数。
2. 方法重载的基本概念
- 方法重载签名:多个函数签名,它们具有相同的名称,但参数的数量或类型不同。
- 方法实现:一个实现部分来处理所有重载签名,通过参数的数量和类型来判断执行哪个逻辑。
3. 使用场景
方法重载通常用于需要根据不同类型的参数执行不同操作的情况。例如:
- 在同一个函数中处理不同的数据类型。
- 根据输入参数的数量或类型执行不同的逻辑。
4. 示例
function greet(person: string): string;
function greet(person: string, age: number): string;
function greet(person: string, age?: number): string {
if (age) {
return `Hello, ${person}. You are ${age} years old.`;
} else {
return `Hello, ${person}!`;
}
}
在这个示例中,greet 函数有两个重载签名,一个接收一个参数 person,另一个接收两个参数 person 和 age。具体的实现通过 age 参数的存在与否来决定返回的内容。
5. 总结
- 方法重载 允许同名的函数根据不同的参数类型或数量执行不同的逻辑。
- 在 TypeScript 中,方法重载通过定义多个函数签名并提供一个实现来实现。
- 这种方式使得函数可以灵活处理多种不同类型的输入,增强代码的可读性和可维护性。
24. TS 中的泛型是什么?
1. 泛型的定义
泛型(Generics) 是 TypeScript 中的一种强大特性,它允许在定义函数、接口或类时不预先指定具体的类型,而是在使用时通过指定具体的类型来传递给这些结构。泛型使得类型能够在使用时灵活地变化,避免了代码重复并提高了类型安全。
2. 泛型的作用
泛型的主要作用是提高代码的可重用性和灵活性。在不牺牲类型安全的情况下,泛型可以让开发者编写更具通用性的代码,而不需要为每种数据类型编写重复的代码。
3. 泛型的使用场景
- 函数:可以通过泛型函数处理不同类型的数据,且保持类型安全。
- 接口:通过泛型接口可以定义一组适用于不同类型的函数或数据结构。
- 类:泛型类使得类可以操作不同类型的数据,而不需要重复定义类的实现。
4. 泛型的语法
- 泛型函数:通过尖括号
<>
定义泛型类型。 - 泛型接口:在接口中使用泛型,使得接口可以接受多种不同类型。
- 泛型类:通过泛型使得类的实例可以操作不同的数据类型。
5. 示例
泛型函数
function identity<T>(value: T): T {
return value;
}
const num = identity(42); // T 被推断为 number
const str = identity("hello"); // T 被推断为 string
泛型接口
interface Box<T> {
value: T;
}
const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: "hello" };
泛型类
class GenericBox<T> {
value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
const box = new GenericBox<number>(42);
console.log(box.getValue()); // 输出 42
25. TypeScript 常用工具类型:Omit、Pick、Partial
工具类型 | 说明 | 应用场景 |
---|---|---|
Omit | 用于从现有类型中排除指定的键,生成一个新的类型。 | 创建新类型时排除不需要的属性。例如在提交表单时排除敏感或不必要的字段。 |
Pick | 用于从现有类型中选取指定的键,生成一个新的类型。 | 选择类型中的特定属性,常用于 API 数据请求或字段映射时,仅选取所需字段。 |
Partial | 用于将类型中的所有属性设为可选属性。 | 创建对象的可选参数类型,避免对象初始化时要求提供所有字段。 |
Omit
Omit
用于从现有类型中排除指定的键,生成一个新的类型。
type Person = {
name: string;
age: number;
address: string;
};
type PersonWithoutAddress = Omit<Person, "address">;
// 结果类型:{ name: string; age: number; }
Pick
Pick
用于从现有类型中选取指定的键,生成一个新的类型。
type Person = {
name: string;
age: number;
address: string;
};
type PersonNameAndAge = Pick<Person, "name" | "age">;
// 结果类型:{ name: string; age: number; }
Partial
Partial
Partial 用于将类型中的所有属性设为可选属性。
type Person = {
name: string;
age: number;
address: string;
};
type PartialPerson = Partial<Person>;
// 结果类型:{ name?: string; age?: number; address?: string }
26. keyof
和 typeof
关键字的作用
1. keyof
关键字
keyof
是 TypeScript 中的一个类型操作符,用于获取一个对象类型的所有键的联合类型。它返回一个类型,该类型包含对象所有属性的名称作为字符串字面量类型。
interface Person {
name: string;
age: number;
}
type PersonKeys = keyof Person; // "name" | "age"
使用场景:
- 用于获取对象类型的所有键,常与映射类型、索引类型一起使用。
2. typeof
关键字
typeof
是 TypeScript 中的一个操作符,用于获取一个值的类型。它返回该值的类型,可以用于变量、常量或字面量类型的类型推断。
const numberValue = 42;
type NumberType = typeof numberValue; // number
使用场景:
- 用于获取一个变量的类型,常用于类型推断或将值的类型应用于其他类型。
3. keyof
和 typeof
的区别
特性 | keyof | typeof |
---|---|---|
作用 | 获取对象的所有键的联合类型 | 获取一个变量或值的类型 |
返回的类型 | 属性名的联合类型(字符串字面量类型) | 变量或值的类型(例如,{ name: string; age: number; } ) |
使用场景 | 用于获取对象的所有属性名类型,常与映射类型结合使用 | 用于获取变量、常量的类型,常用于类型推断 |
4. 总结
keyof
用于获取对象的属性名,并返回一个联合类型,便于进行属性操作。typeof
用于获取变量的类型,提供动态类型推断的能力。- 两者结合使用,可以增强类型系统的灵活性和准确性。
27. 对协变、逆变、双变和抗变的理解?
协变(Covariance)
:在一个类型系统中,如果子类型可以隐式转换为父类型,则称为协变。在协变关系下,子类型的派生程度大于或等于父类型。简单来说,协变允许将派生类的对象赋值给基类的引用。
2. 逆变(Contravariance)
:与协变相反,逆变是指父类型可以隐式转换为子类型。在逆变关系下,子类型的派生程度小于或等于父类型。通常在函数参数上使用逆变,使得可以传入更为特定的类型。
3. 双变(Bivariance)
:双变是同时具备协变和逆变特性的关系。这意味着类型可以在协变和逆变的位置都进行隐式转换。某些类型系统支持双变,但需要谨慎使用,因为它可能导致类型安全问题。
抗变(Invariance)
:抗变是指两个类型之间不存在任何隐式转换。在抗变关系下,类型之间无法互相赋值,除非显式进行类型转换。抗变关系保证了类型的严格一致性。
28. 跟 TypeScript 类似的技术方案还有哪些?
1. TypeScript (TS)
TypeScript 是 JavaScript 的超集,提供了静态类型检查,支持类型注解、接口、泛型等功能。TypeScript 通过静态类型系统帮助开发者在编译时发现类型错误,提升代码质量和可维护性。它广泛应用于大型项目的开发中。
2. Flow
Flow 是一个由 Facebook 开发的静态类型检查工具,用于 JavaScript。Flow 提供了可选的类型注解功能,能够在开发过程中发现类型错误。与 TypeScript 不同,Flow 是一种类型检查器,而不是编程语言的超集,仍然依赖 JavaScript。
- 特点:
- 类型检查是可选的,使用 Flow 时,开发者可以选择性地为代码添加类型注解。
- 支持与现有 JavaScript 代码的兼容性,开发者可以逐步添加类型。
- 提供了类型推导,减少手动类型注解的需要。
3. JSDoc
JSDoc 是一种注释标准,用于为 JavaScript 代码添加类型信息。通过 JSDoc 注释,开发者可以提供类型提示,IDE 和工具可以通过这些注释进行类型检查和自动补全。JSDoc 本身并不是一个类型检查工具,但可以与其他工具(如 TypeScript 或 Flow)结合使用,或者单独通过工具进行静态分析。
- 特点:
- 可以直接在 JavaScript 代码中使用,无需引入额外的编译步骤。
- 适用于没有引入类型检查的项目,通过注释提供基本的类型信息。
- 常用于 JavaScript 库和工具的文档化。
4. Prop-types (React)
Prop-types 是 React 官方提供的一个库,用于对组件的 props
进行类型检查。通过在组件中定义 propTypes
,可以确保传递给组件的 props
符合预期的类型,从而减少运行时错误。
- 特点:
- 提供了基础的类型验证,主要用于 React 组件的开发。
- 运行时检查类型,而不是在编译时进行静态类型检查。
- 尽管 Prop-types 不能提供 TypeScript 或 Flow 那样的静态类型支持,但它能帮助在开发过程中捕获常见的错误。
5. ESLint + TypeScript/Flow
ESLint 是一个静态代码分析工具,通常用于检查 JavaScript 代码中的潜在问题。结合 TypeScript 或 Flow,ESLint 可以提供更强的类型检查功能。
- 特点:
- ESLint 本身不提供类型检查,但可以与 TypeScript 或 Flow 配合使用,分析类型相关的错误。
- 通过 ESLint 插件,开发者可以将 TypeScript 或 Flow 的类型规则集成到代码检查流程中。
- 提供了更细粒度的代码风格和错误检查,帮助保持代码的一致性和质量。
6. PureScript
PureScript 是一种与 Haskell 相似的编程语言,支持强类型系统和函数式编程。它与 JavaScript 兼容,并可以编译为 JavaScript。与 TypeScript 和 Flow 不同,PureScript 是一种全新的语言,适用于希望使用更强类型和更复杂抽象的开发者。
- 特点:
- 强类型和函数式编程特性,适合需要高级类型系统的项目。
- 与 JavaScript 高度兼容,可以与现有的 JavaScript 库和框架一起使用。
- 提供了更为严谨和强大的类型系统。
7. Kotlin/JS
Kotlin 是一种静态类型的编程语言,最初为 JVM 开发,但也可以编译为 JavaScript(Kotlin/JS)。Kotlin 提供了与 TypeScript 类似的类型检查功能,支持类型推导、空安全等特性。
- 特点:
- 支持强
29. 编译配置文件 tsconfig.json
的作用,都有哪些配置?
1. tsconfig.json
的作用
tsconfig.json
是 TypeScript 项目的配置文件,定义了编译器选项、项目文件以及如何执行 TypeScript 编译的行为。它允许开发者指定要编译的文件、编译选项以及其它编译设置。使用 tsconfig.json
文件可以帮助 TypeScript 编译器识别文件和设置,生成符合需求的 JavaScript 代码。
2. tsconfig.json
中的常见配置选项
配置项 | 描述 |
---|---|
compilerOptions | 编译器选项,定义 TypeScript 编译器的行为。 |
include | 指定要包含在编译中的文件或目录。 |
exclude | 指定要排除在编译之外的文件或目录。 |
files | 直接指定要编译的文件列表,而不是通过 include 和 exclude 来决定。 |
extends | 继承一个基础的 tsconfig.json 配置文件。 |
references | 用于设置项目间的引用(适用于项目间的依赖关系)。 |
typeAcquisition | 控制对 JavaScript 文件中自动获取类型定义的方式。 |
tsBuildInfoFile | 指定 TypeScript 生成增量编译信息的存储路径(增量编译)。 |
watchOptions | 监视模式配置,决定如何监视文件变化并重新编译。 |
3. compilerOptions
的常见配置项
配置项 | 描述 |
---|---|
target | 指定编译的 JavaScript 版本,如 ES5 , ES6 , ES2015 , ES2020 等。 |
module | 指定模块系统,如 commonjs , esnext , amd , system 等。 |
lib | 指定编译时包含的库文件,如 ["dom", "es2015"] 。 |
strict | 启用所有严格类型检查选项,如 strictNullChecks 等。 |
esModuleInterop | 启用 ECMAScript 模块与 CommonJS 模块的兼容性。 |
skipLibCheck | 跳过库文件的类型检查,以提高编译速度。 |
allowJs | 允许编译 JavaScript 文件。 |
noEmit | 不生成输出文件,仅进行类型检查。 |
sourceMap | 生成源映射文件,以便调试。 |
outDir | 指定输出目录。 |
rootDir | 指定输入文件的根目录。 |
declaration | 生成对应的 .d.ts 类型声明文件。 |
resolveJsonModule | 允许导入 .json 文件作为模块。 |
noImplicitAny | 禁止隐式 any 类型,强制要求所有变量都有明确的类型声明。 |
skipDefaultLibCheck | 跳过默认库文件的检查。 |
allowSyntheticDefaultImports | 允许从没有默认导出的模块中默认导入。 |
4. 总结
tsconfig.json
配置文件是 TypeScript 项目编译和管理的核心。它通过设置 compilerOptions
以及项目相关的其他配置(如 include
, exclude
等),帮助开发者自定义项目的编译过程。常用配置项如 target
, module
, strict
, esModuleInterop
等,帮助指定编译的输出类型、模块系统、严格模式等行为。
30. declare
和 declare global
区别是什么?
1. declare
关键字
declare
用于声明变量、函数、类、模块等,但不会在代码中生成实际的实现。它告诉 TypeScript 编译器,这个标识符已经在其他地方定义了,并且只需要进行类型检查。
使用场景:
- 用于声明外部库的类型。
- 用于声明全局作用域中已存在的变量或模块。
- 用于给动态代码提供类型定义。
2. declare global
关键字
declare global
是一种特殊的 declare
用法,用于扩展全局作用域。它在一个模块中声明全局变量、类型或接口,使得这些声明在整个应用程序中都可用。declare global
通常用于扩展 global
对象或模块的全局范围。
使用场景:
- 扩展全局命名空间或全局对象。
- 在模块中添加全局类型声明,使其在整个项目中可用。
3. declare
和 declare global
的区别
特性 | declare | declare global |
---|---|---|
作用 | 用于声明变量、函数、类、模块等,告诉编译器其存在 | 用于扩展全局作用域,在模块中声明全局变量或类型 |
使用范围 | 适用于声明模块、外部库、变量等 | 用于在模块中声明全局变量、类型或接口 |
声明的范围 | 仅限于当前文件或作用域 | 在整个项目中都有效 |
1. declare
关键字示例
// declare 声明一个外部变量,告知 TypeScript 该变量的存在和类型
declare var someGlobalVar: string;
someGlobalVar = "Hello, World!";
console.log(someGlobalVar); // "Hello, World!"
在这个示例中,declare
用于声明一个全局变量 someGlobalVar
,并指定它的类型为 string
。这意味着 TypeScript 编译器知道该变量存在,而不需要在代码中实现它。
// declare 声明一个外部模块
declare module "my-library" {
export function myFunction(): void;
}
import { myFunction } from "my-library";
myFunction();
在这个示例中,declare
用于声明一个外部模块 my-library
,并声明其中包含一个 myFunction
函数。declare
告诉 TypeScript 该模块存在并且具有指定的接口。
2. declare global
关键字示例
// declare global 扩展全局作用域
declare global {
var myGlobalVar: string;
}
myGlobalVar = "Global Variable";
console.log(myGlobalVar); // "Global Variable"
在这个示例中,declare global
用于在一个模块中扩展全局作用域,声明一个全局变量 myGlobalVar
,并指定它的类型为 string。这个变量可以在整个项目中的任何地方使用。
// declare global 扩展全局作用域以声明一个接口
declare global {
interface Window {
customProperty: string;
}
}
window.customProperty = "This is a custom property";
console.log(window.customProperty); // "This is a custom property"
在这个示例中,declare global
用于扩展全局作用域并向 Window
接口添加一个新的属性 customProperty
。这使得 window 对象可以包含该新属性。
4. 总结
declare
:用于声明变量、类型、模块等,通常不会生成实际的实现。适用于模块或局部作用域中的声明。declare global
:用于扩展全局作用域,在模块中声明全局变量或类型,使得这些声明在项目中任何地方都可以使用。
31. const
和 readonly
的区别是什么?
1. const
关键字
const
用于声明一个常量,表示该变量的值在赋值后不可更改。- 适用于基本数据类型和引用类型的变量。
- 对于基本数据类型,
const
保证值不能被修改;对于引用类型,const
保证变量的引用不能改变,但对象或数组的内容仍然可以被修改。
2. readonly
关键字
readonly
用于修饰对象类型的属性或数组,表示该属性或数组的元素一旦被初始化后,不能再被修改。- 适用于类的属性或接口的属性,确保对象的不可变性。
3. const
和 readonly
的区别
特性 | const | readonly |
---|---|---|
作用 | 保证变量的引用不可修改,适用于基本数据类型和引用类型的变量 | 保证对象属性或数组元素在初始化后不可修改 |
适用范围 | 适用于变量的声明(包括基本数据类型和引用类型) | 适用于对象的属性或数组的元素 |
能否修改值 | 对基本数据类型不可修改,对引用类型的引用不可修改,但内容可变 | 对对象属性或数组元素不可修改 |
是否可以重新赋值 | 不允许重新赋值 | 允许重新赋值,但不能修改已初始化的属性或元素 |
4. 总结
const
:用于声明不可重新赋值的变量,适用于基本数据类型和引用类型的变量。对于引用类型,const
保证引用不可更改,但对象的内容仍然可以修改。readonly
:用于确保对象属性或数组元素不可修改,适用于对象的属性或数组的元素,确保其初始化后不能被更改。
32. 简单介绍一下 TypeScript 模块的加载机制?
假设有一个导入语句 import { a } from "moduleA"
- 首先,编译器会尝试定位需要导入的模块文件,通过绝对或者相对的路径查找方式
- 如果上面的解析失败了,没有查找到对应的模块,编译器会尝试定位一个外部模块声明
(.d.ts)
- 最后,如果编译器还是不能解析这个模块,则会抛出一个错误
error TS2307: Cannot find module 'moduleA'
33. 简述工具类型 Exclude
、Omit
、Merge
、Intersection
、Overwrite
的作用?
1. Exclude
Exclude
用于从联合类型中排除指定的类型。它返回一个新的类型,该类型由原始类型中排除指定类型后的所有类型组成。
- 语法:
Exclude<T, U>
- 作用:从类型
T
中排除类型U
,返回新的类型。
2. Omit
Omit
用于从一个类型中排除一个或多个属性,返回一个新的类型。它是 Pick
的反操作。
- 语法:
Omit<T, K>
- 作用:从类型
T
中移除K
指定的属性,返回新的类型。
3. Merge
Merge
用于将多个类型合并为一个类型,所有类型的属性都会合并到结果类型中。如果有重复的属性,后面的类型会覆盖前面的类型。
- 语法:
Merge<T, U>
- 作用:合并类型
T
和U
,如果有相同的属性,后者覆盖前者。
4. Intersection
Intersection
用于将多个类型合并为一个类型。结果类型将包含所有类型的属性和方法。
- 语法:
T & U
- 作用:将类型
T
和类型U
合并,返回一个新的包含两者所有属性的类型。
5. Overwrite
Overwrite
用于从类型 T
中选择属性,并用类型 U
中的属性覆盖它们。它的行为与 Intersection
类似,但它会覆盖类型中的指定字段。
- 语法:
Overwrite<T, U>
- 作用:用类型
U
中的字段覆盖类型T
中的相应字段,返回新的类型。
6. 总结
工具类型 | 作用 |
---|---|
Exclude | 从联合类型中排除指定的类型 |
Omit | 从类型中移除一个或多个属性 |
Merge | 合并多个类型,属性重复时后面的类型会覆盖前面的类型 |
Intersection | 将多个类型合并成一个类型,包含所有属性 |
Overwrite | 用类型 U 中的字段覆盖类型 T 中的相应字段 |
34. typescript 代码实现题
// ReadOnly 实现
type MyReadOnly<T> = {
readonly [P in keyof T]: T[P];
};
type Info = {
name: string;
age: number;
};
type myonly = MyReadOnly<Info>;
// Pick 实现
type User = {
name: string;
age: number;
};
type MyPick <T, K extends keyof T> {
[P in K] : T[P]
}
type myPick = MyPick<User, 'name'>
// Omit 实现
type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
type Foot = {
name: string;
price: number;
};
type GreatFoot = MyOmit<Foot, "name">;
// Exclude 实现
type MyExclude<T, U> = T extends U ? never : T;
type test = MyExclude<string | number | boolean, boolean>;