TypeScript面试题

本文最后更新于 2025年10月31日 中午

更多:https://juejin.cn/post/7321542773076082699

TS 和 JS

优点:

  • 静态类型检查。类型错误在 JS 里只能在运行时发现,但是 TS 在编译阶段就能发现,比如访问了不存在的属性,调用方法参数类型不对,这些在写代码时就会报错
  • 更强的 IDE 支持。比如自动补全、跳转到定义和类型推导,开发效率更高
  • 在团队协作中很有价值。因为接口契约清晰,维护成本更低
  • TS 是 JS 的超集,兼容性很好,能用到 ES6+ 的新特性,还提供泛型、枚举、接口等高级功能,让代码更健壮和可扩展

缺点:

  • 学习成本高一点
  • 类型定义初期可能比较繁琐,但长远来看收益更大

any 和 unknown

特性 any unknown
类型检查 不做检查 必须先检查才能使用
安全性 低(可能埋雷) 高(强制开发者做类型守卫)
赋值 可以赋值给任何类型 只能赋值给 anyunknown
使用场景 快速原型、临时绕过类型 接口数据不确定、需要后续收窄类型

两者都能接收任意类型的值,但 any 会关闭类型检查,可以随便用,非常不安全;unknown 更安全,它要求我们在使用之前必须做类型缩小。通常情况下,如果不确定类型推荐用 unknown 而不是 any。

interface 和 type

特性 interface type
定义对象结构 ✅ 支持 ✅ 支持
扩展方式 ✅ 支持(extends ✅ 支持(&
声明合并 ✅ 支持(同名自动合并) ❌ 不支持(再定义同名会报错)
联合/交叉类型 ❌ 不支持 ✅ 支持
元组/基础类型 ❌ 不支持 ✅ 支持
class 实现 ✅ 支持 implements ❌ 不支持
使用场景 面向对象风格,约束对象/类 灵活定义各种复杂类型(联合、条件、元组)

在 TypeScript 中,interface 和 type 都能描述对象结构,但有一些区别:

  • interface 偏于面向对象和类,可用 extends 扩展,可被 class implements,可声明合并
  • type 更灵活,能定义对象结构,以及联合类型、交叉类型、元组和条件类型,但不能声明合并

总体来说,如果是对象和类的契约接口,推荐用 interface;而如果需要更复杂的类型表达式,比如联合类型或工具类型,推荐用 type

never 和 void

特性 void never
含义 没有返回值(可能 undefined/null 不可能有值(函数无法结束/抛错)
使用场景 常用于函数没有返回值 抛异常、死循环、不可能发生的分支
是否能赋值 void 变量可赋值为 undefined/null never 变量无法赋值任何值
编译器语义 “我不关心返回值” “这里绝对不应该到达”

元组和数组

特性 数组(Array) 元组(Tuple)
长度 不固定 固定(或有限制)
元素类型 必须统一(或联合类型) 每个位置类型可不同
适用场景 存放一类数据集合 表示一组有顺序的、结构化的数据
示例 [1, 2, 3] [string, number] → ["Tom", 25]
1
2
3
4
5
6
let tuple: [string, number] = ["Tom", 25];

tuple[0]; // string
tuple[1]; // number
tuple.push("extra"); // ✅(特殊情况 TS 容许,但最好避免)
tuple[2]; // ❌ 不在定义范围内

Omit 和 Pick

  • Pick<T, K> 是从类型 T 里挑选出一部分属性,生成新类型
  • Omit<T, K> 是从类型 T 里排除掉一部分属性,生成新类型

它们的作用正好相反,一个是“取子集”,一个是“去子集”。

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
interface Person {
name: string;
age: number;
gender: string;
}

// Pick 出 name 和 age
type PersonBase = Pick<Person, "name" | "age">;

// Omit 掉 age
type WithoutAge = Omit<Person, "age">;

/*
PersonBase 等价于:
{
name: string;
age: number;
}
*/

/*
WithoutAge 等价于:
{
name: string;
gender: string;
}
*/

联合类型和交叉类型

特性 联合类型 A | B 交叉类型 A & B
关系 或者(并集) 并且(交集)
含义 值可能是几种类型之一 值必须同时符合所有类型
示例 string | number → 字符串或数字 {name} & {age} → 必须有 nameage
使用场景 参数多种类型,灵活 组合多个对象类型,复用性强

类型断言和类型推断

类型断言可以用于手动指定一个值的具体类型,即允许变量从一种类型更改为另一种类型。
类型推断是 TS 根据赋值语句右侧的值自动推断变量的类型。

declare 关键字作用

declare 关键字在 TS 中用来声明已经存在的变量、函数、类、模块等,不需要在 TS 文件中实现。它只在编译时生效,不会生成运行时代码。常见场景:

  • 声明全局变量/函数/类
  • 声明第三方库的类型
  • .d.ts 类型声明文件

TS 支持的访问修饰符

TypeScript 在类中支持 publicprivateprotected 三种访问修饰符:

  • public 表示对外公开,默认值
  • private 表示成员只能在类的内部访问
  • protected 表示成员只能在类和子类中访问

还有 readonly(只读属性)和 static(静态成员)修饰符,用来增强类的封装性和可维护性。

TS 中的泛型是什么

泛型是给类型定义参数,让函数、接口、类在编写时不预设具体类型,而在使用时再确定类型:

  • 普通函数参数:数据的占位符
  • 泛型:类型的占位符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 没有泛型
// any 会让 TS 丢失类型检查,返回值不再安全
function identity(arg: any): any {
return arg;
}

function identity<T>(arg: T): T {
return arg;
}

// 使用泛型
// 保留了参数和返回值的 类型关联,而不是丢失成 any
const a = identity<string>("hello"); // 返回 string
const b = identity<number>(123); // 返回 number

泛型的核心作用:

  • 可重用:一个函数/接口/类,可以适配多种类型
  • 类型安全:编译器会推导类型,减少 any 带来的风险
  • 灵活性:可以在使用时指定或让 TS 自动推导
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
// 泛型函数
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}

// 泛型接口
interface ApiResponse<T> {
code: number;
data: T;
}

const res: ApiResponse<string> = { code: 200, data: "ok" };

// 泛型类
class Stack<T> {
private items: T[] = [];
push(item: T) {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}

// 泛型约束
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
getLength("hello"); // ✅
getLength(123); // ❌ number 没有 length

TS 中的类型有哪些

类型系统表示语言支持的不同类型的值。它在程序存储或者操作所提供的值之前检查其有效性。可以分为两种类型:

  • 内置:包括数字(number),字符串(string),布尔值(boolean),无效(void),空值(null),未定义(undefined),bigint,符号(symbol)
  • 用户定义:包括枚举(enums),类(classes),接口(interfaces),数组(arrays),元组(tuple)

TS 中什么是装饰器

是一种特殊类型的声明,它能过被附加到类声明,方法,属性或者参数上,可以修改类的行为。

通俗的来说就是一个方法,可以注入到类,方法,属性参数上来扩展类,属性,方法,参数的功能。装饰器的分类: 类装饰器、属性装饰器、方法装饰器、参数装饰器等。

解释 TS 的 mixin

在 TS 里,Mixin 是一种通过组合来复用类逻辑的方式。比如我有 Jumpable 和 Runnable 这样的类,我可以把它们混入到 SuperPerson 里,让它同时拥有 jump 和 run 方法。

在 TS 中实现 Mixin 通常有两种方式:一种是用 implements 结合 applyMixins 函数复制原型方法,另一种是更常用的函数式 Mixin,返回一个扩展了基础类的新类。

相比传统继承,Mixin 更灵活,适合为类扩展独立的功能,如在前端项目里给某些对象批量加日志、权限或动画能力。

什么是 TS 映射文件

TS 映射文件就是 Source Map 文件,通常扩展名是 .js.map。当把 TS 编译成 JS 时,TS 编译器可以生成一个映射文件,它的作用是:建立 编译后的 JS 代码 和 原始 TS 代码 的对应关系。方便调试时直接回溯到 TS 源码,而不是看编译后的 JS。

tsconfig.json

tsconfig.json 文件中,可以指定不同的选项来告诉编译器如何编译当前项目。目录中包含 tsconfig.json 文件,表明该目录是 TypeScript 项目的根目录。

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// 常用配置
{
/*
tsconfig.json是ts编译器的配置文件,ts可以根据它的信息来对待吗进行编译 可以再tsconfig中写注释
include : 用来指定哪些文件需要被编译
exclude : 用来指定哪些文件不需要被编译 :默认node_module
extends : 用来指定继承的配置文件
files : 用来指定被编译的文件列表,只有编译少量文件才使用
compilerOptions : 编译器的选项是配置文件中非常重要也是非常复杂的配置选项
*/
"include": [
// ** : 任意目录 , * : 任意文件
"./src/**/*"
],
"exclude": ["./src/hello/**/*"],
"extends": "./configs/base",
"files": [
"1.ts"
// "2.ts"
],
"compilerOptions": {
// 用来指定 ES 版本 ESNext : 最新版。 'ES3', 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ESNext'
"target": "ES2020",
// 指定要使用模块化的规范 : 'None', 'CommonJS', 'AMD', 'System', 'UMD', 'ES6'/'ES2015', 'ES2020' or 'ESNext'
"module": "ESNext",
// 用来指定项目中要使用的库 'ES5', 'ES6', 'ES2015', 'ES7', 'ES2016', 'ES2017', 'ES2018', 'ESNext', 'DOM', 'DOM.Iterable',
// 'WebWorker', 'ScriptHost', 'ES2015.Core', 'ES2015.Collection', 'ES2015.Generator', 'ES2015.Iterable',
// 'ES2015.Promise', 'ES2015.Proxy', 'ES2015.Reflect', 'ES2015.Symbol', 'ES2015.Symbol.WellKnown',
// 'ES2016.Array.Include', 'ES2017.object', 'ES2017.Intl', 'ES2017.SharedMemory', 'ES2017.String',
// 'ES2017.TypedArrays', 'ES2018.Intl', 'ES2018.Promise', 'ES2018.RegExp', 'ESNext.AsyncIterable',
// 'ESNext.Array', 'ESNext.Intl', 'ESNext.Symbol'
// 运行在浏览器中不用设置,运行在node或其他中才需要设置
"lib": [],
// 用来指定编译后文件的存放位置
"outDir": "./dist",
// 将代码合并为一个文件,设置之后所有的全局作用域中的代码会合并到同一个文件中 但是只能在 'amd' and 'system' 中才能使用
// "outFile": "./dist/app.js",
// 是否对js文件进行编译,默认false
"allowJs": false,
// 是否检查js代码是否符合语法规范,默认false
"checkJs": false,
// 是否移除注释,默认false
"removeComments": false,
// 是否不生成编译后文件,默认false
"noEmit": false,
// 当有错误时是否生成文件,默认false
"noEmitOnError": false,
// 是否生成sourceMap,默认false 这个文件里保存的,是转换后代码的位置,和对应的转换前的位置。有了它,出错的时候,通过断点工具可以直接显示原始代码,而不是转换后的代码。
"sourceMap": false,
// 所有的严格检查的总开关,默认false
"strict": false,
// 编译后的文件是否开启严格模式,默认false
"alwaysStrict": false,
// 不允许隐式的any,默认false(允许)
"noImplicitAny": false,
// 不允许隐式的this,默认false(允许)
"noImplicitThis": false,
// 是否严格的检查空值,默认false 检查有可能为null的地方
"strictNullChecks": true,
// 是否严格检查bind、call和apply的参数列表,默认false 检查是否有多余参数
"strictBindCallApply": false,
// 是否严格检查函数的类型,
"strictFunctionTypes": false,
// 是否严格检查属性是否初始化,默认false
"strictPropertyInitialization": false,
// 是否检查switch语句包含正确的break,默认false
"noFallthroughCasesInSwitch": false,
// 检查函数没有隐式的返回值,默认false
"noImplicitReturns": false,
// 是否检查检查未使用的局部变量,默认false
"noUnusedLocals": false,
// 是否检查未使用的参数,默认false
"noUnusedParameters": false,
// 是否检查不可达代码报错,默认false true,忽略不可达代码 false,不可达代码将引起错误
"allowUnreachableCode": false
}
}

命名空间与模块区别

命名空间主要用于全局作用域下的组织代码,防止命名冲突,而模块是基于文件的作用域隔离机制,通过 import/export 实现代码复用。随着 ES6 模块的普及,更推荐使用模块而不是命名空间。

对比点 命名空间(Namespace) 模块(Module)
定义方式 namespace 关键字 一个文件就是一个模块
导入导出 export 导出,在同一文件或全局作用域使用 使用 export / import
使用场景 早期全局作用域代码组织,解决命名冲突 现代前端/Node.js 工程化开发
作用域 命名空间是全局的,容易被打包进一个文件 模块有自己作用域,天然隔离
推荐程度 不推荐(过时,适合小项目/老项目) 强烈推荐(符合 ES6 标准)

TypeScript面试题
https://xuekeven.github.io/2025/08/25/TypeScript面试题/
作者
Keven
发布于
2025年8月25日
更新于
2025年10月31日
许可协议