Skip to content

TypeScript

TypeScript 教程

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
ts
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),它们都可以接受任意类型的值,但它们的安全性和用途完全不同。

一、核心区别总结

特性anyunknown
接受的值任意类型任意类型
类型检查跳过 类型检查(完全放弃)保留 类型检查(类型安全)
能否直接操作✅ 允许对值做任何操作❌ 不允许直接操作,除非先做类型缩小或断言
使用场景快速绕过类型系统(不推荐用于生产)安全地接收未知类型的值(推荐)

二、示例对比

1. any: 放弃类型检查 —— 什么都能做,风险也最大

ts
let value: any = "hello";

value.toUpperCase();     // ✅ 正常运行
value.trim();            // ✅ 正常运行
value.doesNotExist();    // ✅ 编译也不报错(运行时报错)
  • any 是“逃生舱”,绕过了 TS 的类型系统。
  • 使用 any 后,相当于告诉 TS:“我知道我在干嘛,不用你管”。

2. unknown: 类型安全的 any

ts
let value: unknown = "hello";

value.toUpperCase();  // ❌ 报错:对象的类型为 unknown

if (typeof value === "string") {
  console.log(value.toUpperCase()); // ✅ OK
}
  • unknown 是类型安全的:TS 会强制你先判断类型再操作。
  • 用于需要接收任何值,但又想避免错误操作的场景,比如:
    • 表单输入
    • 网络请求结果
    • JSON.parse 返回值

三、在函数参数中使用

ts
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 都可以用来描述:

  • 对象的结构
  • 函数签名
  • 可选属性、只读属性
  • 扩展和组合类型
ts
// interface 定义对象类型
interface User {
  id: number;
  name: string;
}

// type 定义对象类型
type User = {
  id: number;
  name: string;
};

二、区别对比

特性interfacetype
定义对象结构✅ 支持✅ 支持
扩展(继承)方式extends 关键字使用交叉类型 &
可重复声明合并✅ 可以多次声明,会自动合并❌ 不能重复声明(会报错)
联合类型/原始类型❌ 不支持✅ 可以定义联合、原始、元组等类型
推荐使用场景结构化对象、组件 Props、可拓展的 API 定义联合类型、工具类型、复杂类型组合、多样化数据结构等

三、例子对比

✅ 1. 扩展

interface 使用 extends

ts
interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

type 使用交叉类型 &

ts
type Animal = {
  name: string;
};

type Dog = Animal & {
  breed: string;
};

✅ 2. 类型合并(仅 interface 支持)

ts
interface Person {
  name: string;
}

interface Person {
  age: number;
}

// 等价于:
interface Person {
  name: string;
  age: number;
}
ts
type Person = { name: string };
type Person = { age: number }; // ❌ 报错:重复定义

3. 定义联合类型、原始类型(仅 type 支持)

ts
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 用于获取一个变量/常量的类型,并在类型定义中使用它。

ts
const user = {
  id: 1,
  name: "Alice"
};

type User = typeof user;
// 等价于:
type User = {
  id: number;
  name: string;
};

✅ 用于让你不用重复写类型 —— 把已有值当成类型模板。

2.📌 常见使用场景:

  1. 基于已有对象/函数推导类型(避免重复)
ts
const config = {
  port: 8080,
  debug: true,
};

type Config = typeof config;
  1. 配合 ReturnType 使用
ts
function getUser() {
  return { id: 1, name: "Tom" };
}

type User = ReturnType<typeof getUser>; // 推导函数返回值类型

二、keyof —— 获取对象类型的所有键名组成的联合类型

✅ 作用:

keyof T 会返回类型 T 的所有键名的字面量联合类型

ts
type User = {
  id: number;
  name: string;
};

type UserKeys = keyof User; // "id" | "name"

📌 常见使用场景:

  • 索引签名:keyof 可以用来创建索引签名,确保对象的键只能是某些特定的类型。
  • 类型守卫:keyof 可以与类型守卫结合使用,以确保变量的属性访问是安全的。可以使用 keyof 来定义泛型约束,限制泛型参数为某个对象的键。
  • 映射类型:keyof 可以与映射类型结合使用,创建新类型,其属性是原始类型的子集。
  • 条件类型:keyof 可以用于条件类型中,基于类型的键来分支类型。
  • 函数重载:keyof 可以用于函数重载,以区分不同的函数签名。
ts
// 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

ts
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) 是用于构建可复用、可拓展且类型安全的代码的一种强大工具,尤其在函数、类、接口、工具类型中非常常见。

一、什么是泛型?

泛型是类型的占位符,在定义时不指定具体类型,而在使用时再传入具体的类型参数。

示例:

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

identity<string>("hello"); // 显式指定类型
identity(123);             // 类型推导为 number

<T> 是一个泛型参数,可以是任何合法的类型标识符。

二、泛型的使用场景

✅ 1. 通用函数

适用于函数接收不同类型但结构相似的参数:

ts
function wrapInArray<T>(input: T): T[] {
  return [input];
}

wrapInArray(1);        // [1]
wrapInArray("hello");  // ["hello"]

✅ 2. 泛型接口

用于表示灵活的数据结构,如 API 响应格式、组件 Props

ts
interface ApiResponse<T> {
  code: number;
  data: T;
  message: string;
}

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

✅ 3. 泛型类

使类能处理不同类型的数据:

ts
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 泛型

ts
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 属性

ts
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:限定某个字段名

ts
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");  // ❌ 报错

四、多个泛型参数的使用

ts
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 中的 nullundefined
ReturnType<T>获取函数 T 的返回值类型
Parameters<T>获取函数 T 的参数类型组成的元组
ConstructorParameters<T>获取构造函数参数类型组成的元组
InstanceType<T>获取构造函数类型 T 实例化后的类型
Awaited<T>获取 Promise<T> 中 T 的实际类型(TS 4.5+)

📌 示例讲解

1. Partial<T> – 全部可选

ts
type User = { id: number; name: string };
type PartialUser = Partial<User>;
// 相当于:{ id?: number; name?: string }

手写实现

ts
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

2. Required<T> – 全部必填

ts
type User = { id?: number; name?: string };
type RequiredUser = Required<User>;
// { id: number; name: string }

手写实现

ts
type MyRequired<T> = {
  [K in keyof T]-?: T[K]; // `-?` 移除可选标记
};

3.Readonly<T> – 全部只读

ts
type User = { id: number; name: string };
type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string }

手写实现

ts
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

4. Pick<T, K> – 选取部分字段

ts
type User = { id: number; name: string; age: number };
type UserInfo = Pick<User, "id" | "name">;
// { id: number; name: string }

手写实现

ts
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

5. Omit<T, K> – 忽略字段

ts
type User = { id: number; name: string; age: number };
type WithoutAge = Omit<User, "age">;
// { id: number; name: string }

手写实现

ts
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> – 构造对象类型

ts
type Role = "admin" | "user";
type RoleInfo = Record<Role, string>;
// { admin: string; user: string }

手写实现

ts
type MyRecord<K extends keyof any, T> = {
  [P in K]: T;
};

7. Exclude<T, U> – 从联合类型中排除

ts
type A = "a" | "b" | "c";
type B = Exclude<A, "a">; // "b" | "c"

8. Extract<T, U> – 从联合类型中提取

ts
type A = "a" | "b" | "c";
type B = Extract<A, "a" | "c">; // "a" | "c"

9. NonNullable<T> – 去除 null 和 undefined

ts
type A = string | null | undefined;
type B = NonNullable<A>; // string

10. ReturnType<T> - Parameters<T>

ts
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