TypeScript 核心知识点

37 阅读18分钟

一、基础类型与类型注解

1. TypeScript 中的 anyunknownnevervoid 类型有什么区别?分别在什么场景下使用?

解析:

  • any:关闭类型检查,可以赋予任何值,调用任何方法。适合逐步迁移 JavaScript 项目或处理动态数据,但过度使用会丧失类型安全。
  • unknown:是类型安全的 any,表示未知类型。不能直接赋值给其他类型或调用方法,必须先通过类型断言或类型收窄(如 typeofinstanceof)才能使用。适合在无法预知数据类型时使用,强制开发者进行类型检查。
  • never:表示永远不会发生的类型,例如函数抛出异常或无限循环的返回值。在联合类型中用于过滤不可达分支,确保穷尽性检查。
  • void:表示函数没有返回值,或者返回 undefined。通常用于函数返回值类型注解。

示例:

let a: any = 1;
a = 'string'; // 可行
a.toUpperCase(); // 可行

let u: unknown = { name: 'test' };
// u.name; // 错误,unknown 不能直接访问属性
if (typeof u === 'object' && u !== null) {
  console.log((u as { name: string }).name); // 断言后可用
}

function error(msg: string): never {
  throw new Error(msg);
}

function log(): void {
  console.log('hello');
}

2. TypeScript 中 numberNumberstringString 等基本类型包装对象的区别是什么?应该使用哪个?

解析:
number 是 TypeScript 的基本类型,表示原始数值;Number 是 JavaScript 中的构造函数,属于对象类型。在 TypeScript 中应始终使用小写的原始类型(numberstringbooleansymbol 等),而不是它们的包装对象类型。使用包装对象类型会导致意外行为,例如 new Boolean(false) 是一个对象,在条件判断中为真。

示例:

let n1: number = 42;
let n2: Number = 42; // 不推荐,但赋值时原始类型会自动装箱,可以工作
// 但下面的用法会出错:
let b1: boolean = Boolean(0); // false,正确
let b2: boolean = new Boolean(0); // 错误,Type 'Boolean' is not assignable to type 'boolean'

3. 什么是元组(Tuple)?如何定义可选元素和剩余元素?元组与数组有何区别?

解析:
元组是一种固定长度、每个元素类型可以不同的数组。定义时明确列出每个位置的类型。

  • 可选元素用 ? 表示:[string, number?]
  • 剩余元素可以用 ... 表示:[string, ...number[]] 表示第一个元素是字符串,后面任意数量的数字。

元组与数组的区别:数组通常元素类型相同,长度不限;元组长度固定(除非有剩余元素),各位置类型可不同。访问越界索引时,元组会提示错误(除非开启 noUncheckedIndexedAccess)。

示例:

let tuple: [string, number] = ['hello', 42];
// tuple[2] = 10; // 错误,索引超出范围
let optionalTuple: [number, string?] = [1];
let restTuple: [boolean, ...string[]] = [true, 'a', 'b', 'c'];

二、接口与类型别名

4. interfacetype 的区别是什么?在什么情况下应该使用哪个?

解析:
共同点:都可以描述对象类型、函数类型,且可以互相扩展(交叉或继承)。
区别:

  • interface 只能描述对象类型,可以多次声明合并(declaration merging),扩展使用 extends
  • type 可以描述任何类型(原始类型、联合类型、交叉类型、元组等),不能声明合并,但可以通过交叉类型(&)扩展。
  • interface 更适合用于定义公共 API 或需要声明合并的场景(如给第三方库添加类型);type 更适合用于组合复杂类型、联合类型、映射类型等。

最佳实践:优先使用 interface 定义对象形状,当需要联合类型、元组或复杂类型表达式时使用 type

示例:

interface Person {
  name: string;
}
interface Person { // 合并
  age: number;
}

type Point = {
  x: number;
  y: number;
};
type ID = string | number; // 联合类型只能用 type

5. 如何定义一个可索引的类型(例如像数组一样通过数字索引访问,或像字典一样通过字符串索引)?

解析:
使用索引签名。通过 [index: type]: valueType 定义,其中 index 可以是 numberstring。注意数字索引签名返回的类型必须是字符串索引签名返回类型的子类型(因为 JavaScript 中数字索引最终也会转为字符串)。

示例:

interface StringArray {
  [index: number]: string; // 索引是数字,值必须是字符串
}

interface Dictionary {
  [key: string]: any; // 允许任意字符串键
  length: number;     // 可以添加其他明确属性,但必须符合索引签名的类型(any 兼容 number)
}

三、函数类型

6. TypeScript 中如何定义函数类型?包括普通函数、箭头函数、可选参数、默认参数和剩余参数。

解析:
函数类型可以通过两种方式定义:类型别名/接口描述,或直接在函数声明时注解。

  • 可选参数用 ? 表示,必须放在必需参数之后。
  • 默认参数不需要 ?,直接赋值,TypeScript 会根据默认值推断类型。
  • 剩余参数用 ... 表示,类型必须是数组。

示例:

// 类型别名定义函数类型
type Greet = (name: string, age?: number) => string;

// 函数声明
function greet(name: string, age: number = 18, ...hobbies: string[]): string {
  return `Hello ${name}, age ${age}, hobbies ${hobbies.join(', ')}`;
}

7. 什么是函数重载?TypeScript 中的函数重载与一些其他语言(如 Java)的重载有何不同?

解析:
TypeScript 的函数重载允许为同一个函数提供多个类型定义,根据传入参数的不同返回不同的类型。它不同于 Java 等语言的重载(多个实现),TypeScript 的重载只是类型层面的,最终实现只有一个,并且需要兼容所有重载签名。
实现时需要写一个联合类型的实现签名,并在内部根据参数类型进行逻辑分支。

示例:

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any): any {
  return a + b;
}
add(1, 2);      // number
add('1', '2');  // string

四、类与面向对象

8. TypeScript 中类的成员可见性修饰符 publicprivateprotectedreadonly 的作用是什么?与 JavaScript 私有字段 # 的区别?

解析:

  • public:默认,任何地方都可访问。
  • private:只能在当前类内部访问(TypeScript 编译后不强制,运行时仍然可访问)。
  • protected:在当前类和子类内部可访问。
  • readonly:属性只能在声明时或构造函数中初始化,之后只读。

JavaScript 私有字段(#field)是运行时的真正私有,不能在类外部访问,且编译后依然存在(ES2022+)。TypeScript 的 private 只在编译期检查,编译后不保留。

示例:

class Animal {
  public name: string;
  private age: number;
  protected type: string;
  readonly id: number;
  #secret: string; // ES2022 私有字段

  constructor(name: string, age: number, type: string, id: number, secret: string) {
    this.name = name;
    this.age = age;
    this.type = type;
    this.id = id;
    this.#secret = secret;
  }
}

9. 什么是抽象类?与接口有什么区别?什么场景下使用抽象类?

解析:
抽象类使用 abstract 关键字定义,可以包含抽象方法(无实现)和具体实现。抽象类不能被实例化,只能被继承。
接口只能定义结构,不能包含实现,且所有方法都是抽象的。
区别:

  • 抽象类可以提供默认实现和状态(字段),接口不能。
  • 类可以实现多个接口,但只能继承一个抽象类。
  • 抽象类更适用于多个类之间有共享代码的场景;接口适用于定义契约,无关实现。

示例:

abstract class Animal {
  abstract makeSound(): void;
  move(): void {
    console.log('roaming');
  }
}

class Dog extends Animal {
  makeSound() {
    console.log('bark');
  }
}

五、泛型

10. 什么是泛型?请举例说明泛型在函数、接口、类中的应用。

解析:
泛型允许定义函数、接口、类时不预先指定具体类型,而在使用时再指定,增强了代码的复用性和类型安全。

  • 泛型函数:function identity<T>(arg: T): T { return arg; }
  • 泛型接口:interface GenericFn<T> { (arg: T): T; }
  • 泛型类:class Stack<T> { private items: T[] = []; push(item: T) { ... } }

使用时可以显式指定类型(如 identity<number>(1)),或利用类型推断(identity(1) 推断为 number)。


11. 什么是泛型约束?如何实现?为什么需要泛型约束?

解析:
泛型约束通过 extends 关键字限制泛型参数必须满足某个条件,例如必须具有某些属性或继承某个类型。当泛型内部需要访问特定属性或方法时,必须通过约束确保传入的类型支持这些操作。

示例:

interface HasLength {
  length: number;
}
function logLength<T extends HasLength>(arg: T): T {
  console.log(arg.length); // 确保 T 有 length 属性
  return arg;
}
logLength([1, 2]); // 数组有 length
logLength('hello'); // 字符串有 length
// logLength(123); // 错误,number 没有 length

12. 解释 keyoftypeof 在 TypeScript 中的作用,并举例说明如何配合泛型使用。

解析:

  • keyof 获取一个类型的所有键的联合类型。例如 keyof Person 得到 "name" | "age"
  • typeof 在类型上下文中获取变量或属性的类型。例如 const obj = { a: 1 }; type T = typeof obj; 得到 { a: number }

配合泛型可以实现类型安全的属性访问函数:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
const person = { name: 'Alice', age: 30 };
getProperty(person, 'name'); // 正确
// getProperty(person, 'address'); // 错误,address 不在 keyof 中

六、高级类型

13. 什么是联合类型(Union Types)和交叉类型(Intersection Types)?分别用在什么场景?

解析:

  • 联合类型 | 表示值可以是几种类型之一。常用于表示多种可能,例如 string | number。使用联合类型时需通过类型守卫收窄类型。
  • 交叉类型 & 将多个类型合并为一个新类型,该类型拥有所有成员。常用于组合对象类型,例如 A & B 表示同时具有 A 和 B 的属性。交叉类型可能产生 never(如果有冲突类型)。

示例:

type ID = string | number;
function printId(id: ID) {
  if (typeof id === 'string') { /* 收窄 */ }
}

type Name = { name: string };
type Age = { age: number };
type Person = Name & Age; // { name: string; age: number }

14. 什么是类型守卫(Type Guard)?列举你知道的类型守卫方式。

解析:
类型守卫是用于在条件块中收窄变量类型的表达式,运行时检查并影响编译器的类型推断。常见方式:

  • typeof 守卫:typeof x === "string"
  • instanceof 守卫:x instanceof Date
  • in 守卫:"property" in obj
  • 自定义类型守卫(返回 x is Type 的函数):function isFish(pet: Fish | Bird): pet is Fish { return (pet as Fish).swim !== undefined; }
  • 判别式联合(discriminated unions):使用共有字面量属性区分。

15. 什么是可辨识联合(Discriminated Unions)?如何利用它实现模式匹配?

解析:
可辨识联合由多个具有共同字面量属性(tag)的接口类型组成联合,通过检查该属性来收窄类型。常用于处理不同形状的数据,例如 Redux Action。
TypeScript 能够根据 tag 自动收窄,无需额外类型守卫。

示例:

interface Circle {
  kind: 'circle';
  radius: number;
}
interface Square {
  kind: 'square';
  sideLength: number;
}
type Shape = Circle | Square;

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle': return Math.PI * shape.radius ** 2;
    case 'square': return shape.sideLength ** 2;
  }
}

16. 什么是条件类型(Conditional Types)?请举例说明其用法,并解释 infer 关键字的作用。

解析:
条件类型根据类型关系选择类型,形式为 T extends U ? X : Y。可以基于泛型参数动态生成类型。
infer 关键字用于在条件类型中推断类型变量,常用于提取函数返回值类型、Promise 内部类型等。

示例:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// 使用
type Fn = (x: number) => string;
type R = ReturnType<Fn>; // string

type ElementType<T> = T extends (infer U)[] ? U : T;
type E1 = ElementType<number[]>; // number
type E2 = ElementType<string>;   // string

17. 什么是映射类型(Mapped Types)?如何基于现有类型创建新类型(例如将所有属性变为只读或可选)?

解析:
映射类型通过遍历已有类型的键来创建新类型,语法为 { [P in K]: T },常配合 keyof 使用。TypeScript 提供了内置工具类型如 Partial<T>Readonly<T>Pick<T, K> 等,它们都是映射类型的应用。

示例: 实现一个 Readonly

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

映射类型还可以通过 +/- 修饰符增减可选或只读属性。


七、类型推断与上下文类型

18. TypeScript 是如何进行类型推断的?请举例说明上下文类型(Contextual Typing)和最佳通用类型(Best Common Type)。

解析:
TypeScript 根据赋值或表达式推断类型。

  • 最佳通用类型:当从多个表达式中推断类型时,计算一个兼容所有类型的类型。例如 let arr = [0, 1, null]; 推断为 (number | null)[]
  • 上下文类型:根据表达式所在的位置(如函数调用参数、赋值目标)推断类型。例如 window.onmousedown = function(mouseEvent) { ... }mouseEvent 会自动推断为 MouseEvent

上下文类型使得代码更简洁,同时保持类型安全。


19. 什么是类型断言(Type Assertion)?什么时候应该使用它?它与类型转换(Type Casting)有何区别?

解析:
类型断言用于告诉编译器“我知道这个值的类型是什么”,常用于当开发者比编译器更了解类型时。语法:value as Type<Type>value(JSX 中不能用后者)。
类型断言在编译时被擦除,不会进行运行时检查,不同于其他语言中的类型转换(转换是运行时行为)。
应该谨慎使用,仅当无法通过类型收窄实现时,或处理来自第三方库的不准确类型时使用。

示例:

const input = document.getElementById('input') as HTMLInputElement;
input.value; // 类型正确

八、模块与命名空间

20. TypeScript 中模块(Module)和命名空间(Namespace)的区别是什么?现代开发中推荐使用哪个?

解析:

  • 命名空间(namespace)是 TypeScript 早期用于组织代码的方式,通过全局对象实现,可以包含内部模块。支持跨文件合并(三斜线指令)。
  • 模块(import/export)是基于 ES6 模块标准的,每个文件是一个模块,依赖关系清晰,适合现代开发。
  • 命名空间主要用于全局脚本环境或避免命名冲突,模块更适合代码组织和依赖管理。现代开发推荐使用 ES6 模块,命名空间已不推荐(除非在声明文件或某些特殊场景)。

九、声明文件与第三方库类型

21. 如何为没有提供 TypeScript 类型定义的 JavaScript 库添加类型?declare 关键字的作用是什么?

解析:
可以创建声明文件(.d.ts)来描述库的类型。

  • 使用 declare 关键字声明全局变量、函数、模块等,告诉 TypeScript 这些成员存在但无需实现。
  • 对于模块库,可以使用 declare module 'module-name' { ... } 描述其导出。
  • 也可以在项目中安装社区维护的 @types/ 包(如 @types/lodash)。
  • 如果库本身包含类型(如通过 types 字段),则直接使用。

示例:

// 在 globals.d.ts 中
declare const MY_GLOBAL: string;
declare function myLibFn(param: string): number;

// 模块声明
declare module 'some-untyped-lib' {
  export function doSomething(): void;
}

十、工具类型

22. 列举 TypeScript 中常用的内置工具类型,并解释其作用(至少说出 5 个)。

解析:
常用内置工具类型(基于映射类型、条件类型等):

  • Partial<T>:将 T 的所有属性变为可选。
  • Required<T>:将 T 的所有属性变为必选。
  • Readonly<T>:将 T 的所有属性变为只读。
  • 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 的参数类型元组。
  • InstanceType<T>:获取构造函数类型的实例类型。

十一、装饰器

23. 什么是装饰器?TypeScript 中支持哪几种装饰器?如何启用装饰器功能?

解析:
装饰器是一种特殊声明,可以附加到类、方法、属性、参数上,用于修改其行为。目前处于 ECMAScript 提案阶段,TypeScript 通过实验性支持提供。
需要设置编译选项 "experimentalDecorators": true
装饰器类型:

  • 类装饰器
  • 方法装饰器
  • 属性装饰器
  • 参数装饰器
  • 访问器装饰器(getter/setter)

装饰器本质上是一个函数,接收特定的参数,并可以返回新的定义。

示例(类装饰器):

function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}
@sealed
class Greeter {}

十二、TypeScript 配置与编译选项

24. tsconfig.json 中的 targetmodulelibstrict 选项分别控制什么?

解析:

  • target:指定编译后的 JavaScript 版本(如 ES5、ES2015、ES2020),影响语法降级和内置 API 是否可用(配合 lib)。
  • module:指定模块系统(如 CommonJS、ES2015、ESNext),决定编译后的模块语法。
  • lib:指定需要包含的类型定义文件列表,如 ["dom", "es2020"]。如果不指定,会根据 target 引入默认库。
  • strict:开启所有严格类型检查选项,包括 noImplicitAnystrictNullChecksstrictFunctionTypesstrictBindCallApplystrictPropertyInitializationnoImplicitThisalwaysStrict。强烈建议开启,以获得最佳类型安全性。

25. 什么是 strictNullChecks?开启它会对代码产生什么影响?

解析:
strictNullChecks 是 TypeScript 的一个严格模式选项,开启后,nullundefined 不再属于任何类型的子类型,必须显式声明才能赋值给变量(如 string | null)。
开启前,nullundefined 可以赋值给任何类型,容易导致运行时错误。
开启后,可以强制开发者处理可能为空的值,提升代码健壮性。需要配合类型守卫或可选链等特性。

示例:

// 未开启 strictNullChecks
let s: string = null; // 允许

// 开启后
let s: string = null; // 错误
let s2: string | null = null; // 正确

十三、与其他技术结合

26. 在 React 项目中使用 TypeScript,如何定义函数组件的 Props 类型?如何处理 children 的类型?如何使用 useState 的泛型?

解析:

  • 定义函数组件 Props 类型通常使用 interfacetype,然后为组件添加类型注解:const MyComponent: React.FC<Props> 或直接 const MyComponent = (props: Props) => ...
  • React.FC 会隐式包含 children 属性(React.ReactNode),但可能带来一些限制(如不支持泛型组件)。现代推荐显式定义 children 类型。
  • children 的类型通常是 React.ReactNode(包括 React 元素、字符串、数字、布尔值、null、undefined 等)。
  • useState 可以通过泛型指定状态类型,例如 const [count, setCount] = useState<number>(0)。如果初始值能推断出类型,可以省略泛型。

示例:

interface ButtonProps {
  label: string;
  onClick: () => void;
  children?: React.ReactNode;
}
const Button = ({ label, onClick, children }: ButtonProps) => {
  return <button onClick={onClick}>{label}{children}</button>;
};

const [value, setValue] = useState<string>(''); // 显式指定

十四、常见陷阱和最佳实践

27. 以下代码有什么问题?如何修复?

function getLength(obj: string | string[]) {
  return obj.length;
}

解析:
代码本身没有问题,因为 stringstring[] 都有 length 属性。但这是一个考察联合类型常见陷阱的例子:如果联合类型中包含没有 length 的类型(如 string | number),则直接访问 length 会报错。
修复方式:使用类型守卫或确保所有类型都有该属性。
本题的陷阱可能是 obj.length 对于 string 返回字符数,对于数组返回元素个数,符合预期。但更常见的陷阱是直接访问可能不存在的属性,需要类型收窄。


28. 解释 !(非空断言操作符)的作用和风险。何时应该使用它?

解析:
! 后缀操作符用于告诉 TypeScript 编译器,一个变量一定不是 nullundefined,即使类型检查认为可能。它从类型中移除 nullundefined
风险:如果运行时实际值为 nullundefined,会导致运行时错误。应仅在确定值一定存在时使用,例如从 DOM 获取元素且确信存在,或初始化后立即赋值且不会被置空。更好的做法是使用类型守卫或可选链。

示例:

const input = document.getElementById('input')!; // 确定存在
input.value = 'hello';

29. 什么是声明合并(Declaration Merging)?请举例说明。

解析:
声明合并指 TypeScript 将多个同名的声明合并为一个定义。最常用于 interface,可以多次声明同一个接口,TypeScript 会自动合并它们的成员。也支持合并 namespace 与类、函数、枚举等。

示例:

interface Box {
  height: number;
}
interface Box {
  width: number;
}
const box: Box = { height: 10, width: 20 }; // 合法,合并后有两个属性

// 命名空间与类合并
class Animal {}
namespace Animal {
  export let legs = 4;
}
console.log(Animal.legs); // 4

30. 如何在 TypeScript 中实现单例模式?结合 static 和私有构造函数。

解析:
利用私有构造函数阻止外部实例化,通过静态方法返回唯一实例。TypeScript 中可以使用 private constructor()static 属性。

示例:

class Singleton {
  private static instance: Singleton;
  private constructor() { /* 初始化 */ }
  static getInstance(): Singleton {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}
const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
console.log(s1 === s2); // true

以上题目涵盖了 TypeScript 的核心知识点,从基础类型到高级类型,从配置到实际应用。建议面试者不仅要掌握语法,还要理解背后的原理和最佳实践,以便在实际开发中写出类型安全、健壮的代码。