TypeScript
Q1. 什么是动态类型与静态类型?
JavaScript 的类型系统非常弱,而且没有使用限制,运算符可以接受各种类型的值。在语法上,JavaScript 属于动态类型语言。TypeScript 的主要功能是为 JavaScript 添加类型系统,TypeScript 引入了一个更强大、更严格的类型系统,属于静态类型语言。
静态类型优点:
- 有利于代码的静态分析
- 有利于发现错误
- 更好的 IDE 支持,做到语法提示和自动补全
- 提供了代码文档
- 有助于代码重构
静态类型缺点:
- 引入了独立的编译步骤
- 丧失了动态类型的代码灵活性
- 增加了编程工作量
- 更高的学习成本
- 兼容性问题
Q2. TS 有哪些类型?
- boolean
- string
- number
- bigint
- symbol
- null
- undefined
- object
- array
- tuple
- enum
- any
- unknown
- never
- void
JavaScript 的 8 种类型之中,undefined 和 null 其实是两个特殊值,object 属于复合类型,剩下的五种属于原始类型(primitive value),代表最基本的、不可再分的值。
boolean
string
number
bigint
symbol
null
undefined
object
五种原始类型都有对应的包装对象,包装类型:
- Boolean 和 boolean
- String 和 string
- Number 和 number
- BigInt 和 bigint
- Symbol 和 symbol
const s1:String = 'hello'; // 正确
const s2:String = new String('hello'); // 正确
const s3:string = 'hello'; // 正确
const s4:string = new String('hello'); // 报错
const o1:Object = { foo: 0 };
const o2:object = { foo: 0 };
o1.toString() // 正确
o1.foo // 报错
o2.toString() // 正确
o2.foo // 报错
const obj:object = undefined;
obj.toString() // 编译不报错,运行就报错
大写类型同时包含包装对象和字面量两种情况,小写类型只包含字面量,不包含包装对象。建议只使用小写类型,不使用大写类型。因为绝大部分使用原始类型的场合,都是使用字面量,不使用包装对象。而且,TypeScript 把很多内置方法的参数,定义成小写类型,使用大写类型会报错。
大写的 Object 类型代表 JavaScript 语言里面的广义对象。所有可以转成对象的值,都是 Object 类型。除了 undefined 和 null 这两个值不能转为对象,其他任何值都可以赋值给 Object 类型。另外,空对象 {} 是 Object 类型的简写形式,所以使用 Object 时常常用空对象代替。
小写的 object 类型代表 JavaScript 里面的狭义对象,即可以用字面量表示的对象,只包含对象、数组和函数,不包括原始类型的值。大多数时候,我们使用对象类型,只希望包含真正的对象,不希望包含原始类型。所以,建议总是使用小写类型 object,不使用大写类型 Object。
undefined 和 null 既是值,又是类型。变量如果等于 undefined 就表示还没有赋值,如果等于 null 就表示值为空。为了避免这种情况,及早发现错误,TypeScript 提供了一个编译选项 strictNullChecks。只要打开这个选项,undefined 和 null 就不能赋值给其他类型的变量(除了 any 类型和 unknown 类型)。
Q3. any 和 unknown 区别?
any 和 unknown 是 TypeScript 中的两种顶级类型(top type),它们都可以接受任意类型的值,但它们的安全性和用途完全不同。
一、核心区别总结
特性 | any | unknown |
---|---|---|
接受的值 | 任意类型 | 任意类型 |
类型检查 | 跳过 类型检查(完全放弃) | 保留 类型检查(类型安全) |
能否直接操作 | ✅ 允许对值做任何操作 | ❌ 不允许直接操作,除非先做类型缩小或断言 |
使用场景 | 快速绕过类型系统(不推荐用于生产) | 安全地接收未知类型的值(推荐) |
二、示例对比
1. any: 放弃类型检查 —— 什么都能做,风险也最大
let value: any = "hello";
value.toUpperCase(); // ✅ 正常运行
value.trim(); // ✅ 正常运行
value.doesNotExist(); // ✅ 编译也不报错(运行时报错)
- any 是“逃生舱”,绕过了 TS 的类型系统。
- 使用 any 后,相当于告诉 TS:“我知道我在干嘛,不用你管”。
2. unknown: 类型安全的 any
let value: unknown = "hello";
value.toUpperCase(); // ❌ 报错:对象的类型为 unknown
if (typeof value === "string") {
console.log(value.toUpperCase()); // ✅ OK
}
- unknown 是类型安全的:TS 会强制你先判断类型再操作。
- 用于需要接收任何值,但又想避免错误操作的场景,比如:
- 表单输入
- 网络请求结果
- JSON.parse 返回值
三、在函数参数中使用
function logAny(value: any) {
// 你可以做任何事(也可能引发异常)
console.log(value.toFixed()); // 不确定是否为 number,编译不报错
}
function logUnknown(value: unknown) {
// 需要先缩小类型
if (typeof value === 'number') {
console.log(value.toFixed()); // ✅ 类型安全
}
}
四、总结一句话记忆
- ✅ 用 unknown 表达「我不知道它是什么,但我会小心处理」
- ❌ 用 any 表达「我不知道它是什么,也不关心错了没」
如果你在写库、写通用函数、接收第三方输入,优先用 unknown 而不是 any,可以显著提升类型安全性。如果你需要了解 unknown 和 never、void 等类型的关系,我也可以进一步解释。
Q4. interface 接口和 type 类型别名区别?
interface 和 type 是 TypeScript 中都可以用来定义类型的关键字,功能有重叠,但也存在一些关键区别,适用于不同的场景。
一、相同点
interface 和 type 都可以用来描述:
- 对象的结构
- 函数签名
- 可选属性、只读属性
- 扩展和组合类型
// interface 定义对象类型
interface User {
id: number;
name: string;
}
// type 定义对象类型
type User = {
id: number;
name: string;
};
二、区别对比
特性 | interface | type |
---|---|---|
定义对象结构 | ✅ 支持 | ✅ 支持 |
扩展(继承)方式 | extends 关键字 | 使用交叉类型 & |
可重复声明合并 | ✅ 可以多次声明,会自动合并 | ❌ 不能重复声明(会报错) |
联合类型/原始类型 | ❌ 不支持 | ✅ 可以定义联合、原始、元组等类型 |
推荐使用场景 | 结构化对象、组件 Props、可拓展的 API 定义 | 联合类型、工具类型、复杂类型组合、多样化数据结构等 |
三、例子对比
✅ 1. 扩展
interface 使用 extends
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
type 使用交叉类型 &
type Animal = {
name: string;
};
type Dog = Animal & {
breed: string;
};
✅ 2. 类型合并(仅 interface 支持)
interface Person {
name: string;
}
interface Person {
age: number;
}
// 等价于:
interface Person {
name: string;
age: number;
}
type Person = { name: string };
type Person = { age: number }; // ❌ 报错:重复定义
3. 定义联合类型、原始类型(仅 type 支持)
type ID = number | string;
type Callback = () => void;
type Point = [number, number];
这些 type 能做的事情 interface 是做不到的。
四、最佳实践建议(2025 年社区主流用法)
使用场景 | 建议用法 |
---|---|
对象结构、类、Props、API 数据结构 | ✅ interface (支持合并、继承) |
联合类型、原始类型、复杂组合类型 | ✅ type |
编写工具类型、泛型组合 | ✅ type |
五、一句话总结
- 如果你在定义对象结构或接口,优先用 interface;
- 如果你在定义联合类型、工具类型、复杂类型组合,用 type。
Q5. keyof 和 typeof 关键字的作用?keyof 使用场景?
keyof 和 typeof 是 TypeScript 中两个非常重要的类型工具关键字,常用于类型推导、泛型约束、工具类型实现中。
下面分别详细讲解它们的作用和常见使用场景。
一、typeof —— 从值中提取类型
1. ✅ 作用
typeof 用于获取一个变量/常量的类型,并在类型定义中使用它。
const user = {
id: 1,
name: "Alice"
};
type User = typeof user;
// 等价于:
type User = {
id: number;
name: string;
};
✅ 用于让你不用重复写类型 —— 把已有值当成类型模板。
2.📌 常见使用场景:
- 基于已有对象/函数推导类型(避免重复)
const config = {
port: 8080,
debug: true,
};
type Config = typeof config;
- 配合 ReturnType 使用
function getUser() {
return { id: 1, name: "Tom" };
}
type User = ReturnType<typeof getUser>; // 推导函数返回值类型
二、keyof —— 获取对象类型的所有键名组成的联合类型
✅ 作用:
keyof T 会返回类型 T 的所有键名的字面量联合类型。
type User = {
id: number;
name: string;
};
type UserKeys = keyof User; // "id" | "name"
📌 常见使用场景:
- 索引签名:keyof 可以用来创建索引签名,确保对象的键只能是某些特定的类型。
- 类型守卫:keyof 可以与类型守卫结合使用,以确保变量的属性访问是安全的。可以使用 keyof 来定义泛型约束,限制泛型参数为某个对象的键。
- 映射类型:keyof 可以与映射类型结合使用,创建新类型,其属性是原始类型的子集。
- 条件类型:keyof 可以用于条件类型中,基于类型的键来分支类型。
- 函数重载:keyof 可以用于函数重载,以区分不同的函数签名。
// 1. 索引签名:keyof 可以用来创建索引签名,确保对象的键只能是某些特定的类型。
interface Person {
name: string;
age: number;
}
// 使用 keyof 创建索引签名
type PersonIndexSignature = {
[K in keyof Person]?: Person[K];
};
const personData: PersonIndexSignature = {
name: "John Doe",
age: 30
};
// 2. 类型守卫:keyof 可以与类型守卫结合使用,以确保变量的属性访问是安全的。
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const person: Person = { name: "Jane", age: 25 };
const name: string = getProperty(person, "name"); // OK
const age: number = getProperty(person, "age"); // OK
// const unknownProperty: any = getProperty(person, "unknownKey"); // Error
// 3. 映射类型:keyof 可以与映射类型结合使用,创建新类型,其属性是原始类型的子集。
type EventNames = "click" | "scroll" | "mousemove";
type EventListeners<T extends EventNames> = {
[K in T]?: (event: any) => void;
};
const listeners: EventListeners<EventNames> = {
click: (event) => console.log("Clicked!"),
scroll: (event) => console.log("Scrolled!"),
};
// 4. 条件类型:keyof 可以用于条件类型中,基于类型的键来分支类型。
type OptionalKeys<T> = {
[K in keyof T]?: T[K];
};
interface Todo {
title: string;
description?: string;
completed: boolean;
}
type TodoOptionals = OptionalKeys<Todo>;
// { title?: string; description?: string; completed?: boolean }
// 5. 函数重载:keyof 可以用于函数重载,以区分不同的函数签名。
function getValue<T>(obj: T, key: keyof T): T[K] {
return obj[key];
}
// 可以用于具有不同属性的对象
const user: { name: string; age: number } = { name: "Alice", age: 25 };
console.log(getValue(user, "name")); // string
console.log(getValue(user, "age")); // number
三、配合使用:typeof + keyof
const person = {
name: "Tom",
age: 30,
};
type Person = typeof person;
type PersonKeys = keyof Person; // "name" | "age
四、一句话总结记忆
关键字 | 功能概述 | 举例 |
---|---|---|
typeof | 获取变量/函数的值对应的类型 | type T = typeof value |
keyof | 获取类型的所有字段名组成的联合类型 | type K = keyof SomeType |
Q6. 泛型使用场景及泛型约束?
TypeScript 中的 泛型(Generics)
是用于构建可复用、可拓展且类型安全的代码的一种强大工具,尤其在函数、类、接口、工具类型中非常常见。
一、什么是泛型?
泛型是类型的占位符
,在定义时不指定具体类型,而在使用时再传入具体的类型参数。
示例:
function identity<T>(value: T): T {
return value;
}
identity<string>("hello"); // 显式指定类型
identity(123); // 类型推导为 number
<T>
是一个泛型参数,可以是任何合法的类型标识符。
二、泛型的使用场景
✅ 1. 通用函数
适用于函数接收不同类型但结构相似的参数:
function wrapInArray<T>(input: T): T[] {
return [input];
}
wrapInArray(1); // [1]
wrapInArray("hello"); // ["hello"]
✅ 2. 泛型接口
用于表示灵活的数据结构,如 API 响应格式、组件 Props
interface ApiResponse<T> {
code: number;
data: T;
message: string;
}
const res: ApiResponse<string> = {
code: 200,
data: "Success",
message: "OK"
};
✅ 3. 泛型类
使类能处理不同类型的数据:
class Stack<T> {
private items: T[] = [];
push(item: T) {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const stringStack = new Stack<string>();
stringStack.push("hello");
✅ 4. React 组件 Props 泛型
type ListProps<T> = {
items: T[];
renderItem: (item: T) => JSX.Element;
};
function List<T>({ items, renderItem }: ListProps<T>) {
return <div>{items.map(renderItem)}</div>;
}
三、泛型约束(extends)
为了防止泛型“太泛”,可以通过 extends 添加类型约束。
示例 1:要求传入的类型必须包含 length 属性
function logLength<T extends { length: number }>(val: T): void {
console.log(val.length);
}
logLength("hello"); // ✅ string 有 length
logLength([1, 2, 3]); // ✅ array 有 length
logLength(123); // ❌ number 没有 length
示例 2:限定某个字段名
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Alice" };
getProperty(user, "id"); // ✅
getProperty(user, "email"); // ❌ 报错
四、多个泛型参数的使用
function merge<T, U>(a: T, b: U): T & U {
return { ...a, ...b };
}
merge({ name: "Tom" }, { age: 30 });
// 返回类型为 { name: string; age: number }
🔧 小贴士(常见的泛型工具类型):
类型工具 | 功能 |
---|---|
Partial<T> | 将类型 T 的所有属性设为可选 |
Required<T> | 将类型 T 的所有属性设为必填 |
Pick<T, K> | 选取 T 中的部分字段 |
Record<K, T> | 用联合类型 K 构造一个对象类型 |
ReturnType<T> | 获取函数返回值类型 |
Q7. 常用的类型工具?
TypeScript 提供了一系列内置的类型工具(Utility Types)
,用于快速构造和转换复杂类型,在日常开发中非常实用,尤其在处理对象、函数、组件 Props、接口继承等场景中。
✅ 常用类型工具大全
工具类型 | 功能说明 |
---|---|
Partial<T> | 将 T 的所有属性变为可选(? ) |
Required<T> | 将 T 的所有属性变为必填(去除 ? ) |
Readonly<T> | 将 T 的所有属性变为只读(readonly ) |
Pick<T, K> | 从 T 中挑选一组属性 K 组成新类型 |
Omit<T, K> | 从 T 中删除一组属性 K 组成新类型 |
Record<K, T> | 构造一个以 K 为键、T 为值的对象类型 |
Exclude<T, U> | 从 T 中排除可以赋值给 U 的类型 |
Extract<T, U> | 从 T 中提取可以赋值给 U 的类型 |
NonNullable<T> | 去除 T 中的 null 和 undefined |
ReturnType<T> | 获取函数 T 的返回值类型 |
Parameters<T> | 获取函数 T 的参数类型组成的元组 |
ConstructorParameters<T> | 获取构造函数参数类型组成的元组 |
InstanceType<T> | 获取构造函数类型 T 实例化后的类型 |
Awaited<T> | 获取 Promise<T> 中 T 的实际类型(TS 4.5+) |
📌 示例讲解
1. Partial<T>
– 全部可选
type User = { id: number; name: string };
type PartialUser = Partial<User>;
// 相当于:{ id?: number; name?: string }
手写实现
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
2. Required<T>
– 全部必填
type User = { id?: number; name?: string };
type RequiredUser = Required<User>;
// { id: number; name: string }
手写实现
type MyRequired<T> = {
[K in keyof T]-?: T[K]; // `-?` 移除可选标记
};
3.Readonly<T>
– 全部只读
type User = { id: number; name: string };
type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string }
手写实现
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
4. Pick<T, K>
– 选取部分字段
type User = { id: number; name: string; age: number };
type UserInfo = Pick<User, "id" | "name">;
// { id: number; name: string }
手写实现
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
5. Omit<T, K>
– 忽略字段
type User = { id: number; name: string; age: number };
type WithoutAge = Omit<User, "age">;
// { id: number; name: string }
手写实现
type MyOmit<T, K extends keyof any> = {
[P in Exclude<keyof T, K>]: T[P];
};
// Omit<T, K> = Pick<T, Exclude<keyof T, K>>
6. Record<K, T>
– 构造对象类型
type Role = "admin" | "user";
type RoleInfo = Record<Role, string>;
// { admin: string; user: string }
手写实现
type MyRecord<K extends keyof any, T> = {
[P in K]: T;
};
7. Exclude<T, U>
– 从联合类型中排除
type A = "a" | "b" | "c";
type B = Exclude<A, "a">; // "b" | "c"
8. Extract<T, U>
– 从联合类型中提取
type A = "a" | "b" | "c";
type B = Extract<A, "a" | "c">; // "a" | "c"
9. NonNullable<T>
– 去除 null 和 undefined
type A = string | null | undefined;
type B = NonNullable<A>; // string
10. ReturnType<T>
- Parameters<T>
function getUser(id: number): { id: number; name: string } {
return { id, name: "Tom" };
}
type User = ReturnType<typeof getUser>; // { id: number; name: string }
type Args = Parameters<typeof getUser>; // [id: number]
🔧 实战小贴士
在 React 中常用于:
Partial<T>
:在表单初始化、props 默认值等场景Omit<T, K>
:在继承组件 Props 时排除某些属性Record<string, any>
:构造表单、配置项等 map 类型对象Parameters<typeof fn>
:包装函数或做函数拦截器ReturnType<typeof fn>
:自动推导返回值类型,避免重复定义
🎯 总结记忆口诀
- 改结构 → Partial、Required、Readonly
- 改内容 → Pick、Omit、Record
- 做集合 → Exclude、Extract、NonNullable
- 函数类 → ReturnType、Parameters、InstanceType
Q8. TypeScript 中 const 与 readonly 的区别?
在 TypeScript 中,const 和 readonly 都用于限制值的修改,但它们适用的范围和作用时机不同。
✅ 一句话区别
关键字 | 用于 | 限制什么 | 作用时机 |
---|---|---|---|
const | 变量(值) | 禁止变量重新赋值 | 运行时限制 |
readonly | 对象的属性(类型) | 禁止属性被修改 | 编译时限制 |
Q9. JIT 与 AOT 编译区别?
JIT(Just-In-Time)
和 AOT(Ahead-Of-Time)
是两种编译策略,它们广泛应用于不同语言(如 Java、TypeScript、Angular、Rust 等)中,用于优化程序执行效率。
✅ 一句话区别
编译方式 | 编译时机 | 执行效率 | 应用场景(代表技术) |
---|---|---|---|
JIT | 运行时即时编译 | 首次较慢,后续快 | Java、V8(JavaScript)、Angular 开发模式 |
AOT | 编译前就生成机器代码 | 启动快、执行快 | C/C++、Rust、Angular 生产模式 |
🔍 详细解释
✅ JIT(Just-In-Time,实时编译)
运行时
将源码或中间码(如字节码)即时编译成机器码。- 通常发生在程序第一次运行时。
- 优点:
- 支持动态优化(如热路径优化、内联函数)
- 允许运行时反射、动态代码生成
- 缺点:
- 首次运行慢(需要编译时间)
- 运行时需要编译器,增加包体和资源占用
🌟 举例:
- Java 的 JVM:将 .class 字节码运行时编译为机器码。
- JavaScript 引擎(如 V8):运行 JS 代码时实时优化热代码。
- Angular 的开发模式:使用 JIT 编译模板,调试更方便。
✅ AOT(Ahead-Of-Time,预编译)
- 在
构建阶段
就将代码编译为目标平台可执行的机器码或优化后的 JavaScript。 - 不依赖运行时编译器。
- 优点:
- 启动快(已预编译)
- 文件体积更小(去除编译器代码)
- 安全性更高(代码不可被修改)
- 缺点:
- 编译过程更长
- 缺乏灵活的运行时动态特性
🌟 举例:
- C/C++:编译为 .exe 或 .out
- Rust:完全静态编译
- Angular 生产模式:使用 AOT 生成优化后的 JavaScript