TypeScript 面试题

13 阅读16分钟

TypeScript 面试题

TypeScript 是 JavaScript 的超集,添加了静态类型系统。以下涵盖 TS 核心概念、类型体操、工程实践等高频面试题。


1. TypeScript 是什么?为什么要使用 TypeScript?

TypeScript 是由微软开发的 JavaScript 超集,它在 JS 基础上添加了静态类型系统编译时类型检查

// JavaScript:运行时才会发现错误
function add(a, b) {
  return a + b
}
add('1', 2)  // '12'(字符串拼接,不是数学加法)——运行时才发现 bug

// TypeScript:编译时就能发现错误
function add(a: number, b: number): number {
  return a + b
}
add('1', 2)  // ❌ 编译错误:类型"string"的参数不能赋给类型"number"的参数

使用 TypeScript 的优势:

优势说明
类型安全编译时捕获类型错误,减少运行时 bug
智能提示IDE 自动补全、参数提示、跳转定义
代码可读性类型即文档,新人接手更容易理解
重构安全修改接口/类型后,编译器自动报告所有受影响的地方
大型项目协作接口约束、模块边界清晰
生态支持主流框架(Vue3、React、Angular)都已原生支持 TS

💡 面试加分点: TypeScript 的类型只存在于编译阶段,编译后生成的 JS 代码中没有任何类型信息,不会影响运行时性能。


2. TypeScript 的基础类型有哪些?

// ========== 基础类型 ==========
let isDone: boolean = false
let count: number = 42
let name: string = '张三'
let u: undefined = undefined
let n: null = null
let big: bigint = 100n
let sym: symbol = Symbol('id')

// ========== 数组 ==========
let arr1: number[] = [1, 2, 3]
let arr2: Array<string> = ['a', 'b', 'c']  // 泛型写法

// ========== 元组(Tuple):固定长度和类型的数组 ==========
let tuple: [string, number] = ['张三', 25]
// tuple = [25, '张三']  // ❌ 类型顺序不对

// 带标签的元组(增强可读性)
type UserInfo = [name: string, age: number, active: boolean]
const user: UserInfo = ['张三', 25, true]

// ========== 枚举(Enum)==========
enum Direction {
  Up = 0,
  Down = 1,
  Left = 2,
  Right = 3
}
let dir: Direction = Direction.Up  // 0

// 字符串枚举
enum Status {
  Active = 'ACTIVE',
  Inactive = 'INACTIVE',
  Pending = 'PENDING'
}

// const 枚举(编译后内联,不会生成对象)
const enum Color {
  Red = 'RED',
  Green = 'GREEN',
  Blue = 'BLUE'
}
let c = Color.Red  // 编译后直接变成 'RED'

// ========== any / unknown / never / void ==========
let anyVal: any = 'hello'      // 任意类型(跳过类型检查,尽量避免)
anyVal.foo.bar                  // ✅ 不报错,但运行时可能崩溃

let unknownVal: unknown = 'hello'  // 安全的 any
// unknownVal.foo               // ❌ 不能直接使用
if (typeof unknownVal === 'string') {
  unknownVal.toUpperCase()      // ✅ 类型收窄后可以使用
}

function throwError(msg: string): never {
  throw new Error(msg)          // never:永远不会有返回值
}

function log(msg: string): void {
  console.log(msg)              // void:没有返回值(但函数会正常结束)
}

💡 面试加分点: anyunknown 的核心区别——any 完全跳过类型检查,unknown 必须先做类型收窄(type narrowing)才能使用。生产项目应优先用 unknown 代替 any


3. interface 和 type 的区别?

// ========== interface:定义对象形状 ==========
interface User {
  name: string
  age: number
  email?: string          // 可选属性
  readonly id: number     // 只读属性
}

// interface 可以被 extends 继承
interface Admin extends User {
  role: 'admin' | 'superadmin'
  permissions: string[]
}

// interface 同名会自动合并(声明合并)
interface Window {
  myCustomProp: string
}
// 现在 window.myCustomProp 是合法的

// ========== type:类型别名 ==========
type ID = string | number       // 联合类型
type Point = { x: number; y: number }
type Callback = (data: string) => void  // 函数类型

// type 可以做 interface 做不了的事
type StringOrNumber = string | number   // 联合类型
type Pair<T> = [T, T]                   // 元组类型
type Keys = keyof User                  // 提取键名类型
type Mapped = { [K in Keys]: boolean }  // 映射类型

// type 也可以用交叉类型实现继承
type Employee = User & {
  department: string
  salary: number
}

interface vs type 对比:

特性interfacetype
对象形状
extends 继承❌(用 & 交叉类型代替)
implements 实现
声明合并✅(同名自动合并)❌(同名报错)
联合类型type A = B | C
元组类型type T = [string, number]
映射类型type M = { [K in Keys]: V }
条件类型type T = A extends B ? C : D

💡 面试加分点: 一般建议——定义对象形状用 interface,定义联合类型、元组、条件类型等复杂类型用 type。Vue3 源码中大量使用 interface;React 社区更偏好 type


4. 什么是泛型(Generics)?

泛型是 TypeScript 最强大的特性之一,它允许在定义函数、接口或类时不预先确定具体类型,而是在使用时再指定。

// ========== 泛型函数 ==========
// 不用泛型:要么用 any(丢失类型),要么写多个重载
function identity<T>(value: T): T {
  return value
}
identity<string>('hello')   // 返回类型是 string
identity<number>(42)        // 返回类型是 number
identity('hello')           // 自动推断 T = string

// ========== 泛型约束(extends)==========
interface HasLength {
  length: number
}

function logLength<T extends HasLength>(value: T): T {
  console.log(value.length)  // ✅ 可以安全访问 length
  return value
}
logLength('hello')       // ✅ string 有 length
logLength([1, 2, 3])     // ✅ 数组有 length
// logLength(123)         // ❌ number 没有 length

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

type UserResponse = ApiResponse<{ name: string; age: number }>
type ListResponse<T> = ApiResponse<{ list: T[]; total: number }>

// 实际使用
async function fetchUser(): Promise<UserResponse> {
  const res = await fetch('/api/user')
  return res.json()
}

// ========== 泛型类 ==========
class Stack<T> {
  private items: T[] = []

  push(item: T): void {
    this.items.push(item)
  }

  pop(): T | undefined {
    return this.items.pop()
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1]
  }

  get size(): number {
    return this.items.length
  }
}

const numStack = new Stack<number>()
numStack.push(1)
numStack.push(2)
numStack.pop()  // 类型是 number | undefined

// ========== 泛型默认值 ==========
interface PaginationParams<T = any> {
  page: number
  pageSize: number
  filters?: T
}

// 使用默认值
const params1: PaginationParams = { page: 1, pageSize: 10 }
// 指定具体类型
const params2: PaginationParams<{ status: string }> = {
  page: 1,
  pageSize: 10,
  filters: { status: 'active' }
}

// ========== 多个泛型参数 ==========
function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]]
}
swap(['hello', 42])  // [42, 'hello'],类型是 [number, string]

// ========== keyof 约束 ==========
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const person = { name: '张三', age: 25 }
getProperty(person, 'name')   // 返回 string
getProperty(person, 'age')    // 返回 number
// getProperty(person, 'email')  // ❌ 'email' 不是 person 的键

💡 面试加分点: 泛型的核心价值是类型安全 + 代码复用。React 中 useState<T>useRef<T>,Vue3 中 ref<T>defineProps<T> 都大量使用泛型。


5. TypeScript 的类型收窄(Type Narrowing)有哪些方式?

// ========== 1. typeof 类型守卫 ==========
function process(value: string | number) {
  if (typeof value === 'string') {
    return value.toUpperCase()   // 这里 value 被收窄为 string
  }
  return value.toFixed(2)        // 这里 value 被收窄为 number
}

// ========== 2. instanceof 类型守卫 ==========
class Dog { bark() { return '汪汪' } }
class Cat { meow() { return '喵喵' } }

function speak(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    return animal.bark()   // Dog 类型
  }
  return animal.meow()     // Cat 类型
}

// ========== 3. in 操作符 ==========
interface Fish { swim: () => void }
interface Bird { fly: () => void }

function move(animal: Fish | Bird) {
  if ('swim' in animal) {
    animal.swim()   // Fish 类型
  } else {
    animal.fly()    // Bird 类型
  }
}

// ========== 4. 可辨识联合类型(Discriminated Unions)==========
// 最常用、最强大的收窄方式
interface Circle {
  kind: 'circle'    // 辨识字段
  radius: number
}
interface Rectangle {
  kind: 'rectangle'  // 辨识字段
  width: number
  height: number
}
interface Triangle {
  kind: 'triangle'
  base: number
  height: number
}

type Shape = Circle | Rectangle | Triangle

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2      // Circle 类型
    case 'rectangle':
      return shape.width * shape.height        // Rectangle 类型
    case 'triangle':
      return 0.5 * shape.base * shape.height   // Triangle 类型
    default:
      const _exhaustive: never = shape  // 穷尽检查
      return _exhaustive
  }
}

// ========== 5. 自定义类型守卫(is 关键字)==========
interface ApiError {
  code: number
  message: string
}

function isApiError(error: unknown): error is ApiError {
  return (
    typeof error === 'object' &&
    error !== null &&
    'code' in error &&
    'message' in error
  )
}

async function fetchData() {
  try {
    const res = await fetch('/api/data')
    return await res.json()
  } catch (error) {
    if (isApiError(error)) {
      console.log(`API 错误 ${error.code}: ${error.message}`)  // 安全访问
    }
    throw error
  }
}

// ========== 6. 断言函数(asserts)==========
function assertIsString(val: unknown): asserts val is string {
  if (typeof val !== 'string') {
    throw new Error(`Expected string, got ${typeof val}`)
  }
}

function processInput(input: unknown) {
  assertIsString(input)
  // 这之后 input 的类型是 string
  console.log(input.toUpperCase())
}

💡 面试加分点: 可辨识联合类型 + switch 的 never 穷尽检查是大型项目中最佳实践——当新增 Shape 类型时,编译器会在所有 switch 处报错,确保不遗漏。


6. TypeScript 的内置工具类型(Utility Types)有哪些?

interface User {
  id: number
  name: string
  email: string
  age: number
  avatar?: string
}

// ========== Partial<T>:所有属性变为可选 ==========
type PartialUser = Partial<User>
// { id?: number; name?: string; email?: string; age?: number; avatar?: string }
// 场景:更新接口只传部分字段
function updateUser(id: number, updates: Partial<User>) {
  // ...
}
updateUser(1, { name: '李四' })  // ✅ 只更新 name

// ========== Required<T>:所有属性变为必填 ==========
type RequiredUser = Required<User>
// avatar 也变为必填了

// ========== Readonly<T>:所有属性变为只读 ==========
type ReadonlyUser = Readonly<User>
const user: ReadonlyUser = { id: 1, name: '张三', email: 'a@b.com', age: 25 }
// user.name = '李四'  // ❌ 只读属性不能修改

// ========== Pick<T, K>:从 T 中选取部分属性 ==========
type UserBasic = Pick<User, 'id' | 'name'>
// { id: number; name: string }

// ========== Omit<T, K>:从 T 中排除部分属性 ==========
type UserWithoutEmail = Omit<User, 'email'>
// { id: number; name: string; age: number; avatar?: string }

// ========== Record<K, V>:构造键值对类型 ==========
type Roles = 'admin' | 'editor' | 'viewer'
type RolePermissions = Record<Roles, string[]>
const permissions: RolePermissions = {
  admin: ['read', 'write', 'delete'],
  editor: ['read', 'write'],
  viewer: ['read']
}

// ========== Exclude<T, U>:从联合类型中排除 ==========
type AllStatus = 'active' | 'inactive' | 'pending' | 'deleted'
type ActiveStatus = Exclude<AllStatus, 'deleted' | 'inactive'>
// 'active' | 'pending'

// ========== Extract<T, U>:从联合类型中提取 ==========
type StringStatus = Extract<AllStatus, 'active' | 'pending'>
// 'active' | 'pending'

// ========== NonNullable<T>:排除 null 和 undefined ==========
type MaybeString = string | null | undefined
type DefiniteString = NonNullable<MaybeString>  // string

// ========== ReturnType<T>:获取函数返回类型 ==========
function fetchUsers() {
  return [{ id: 1, name: '张三' }]
}
type Users = ReturnType<typeof fetchUsers>
// { id: number; name: string }[]

// ========== Parameters<T>:获取函数参数类型 ==========
function createUser(name: string, age: number, email: string) { }
type CreateUserParams = Parameters<typeof createUser>
// [string, number, string]

// ========== Awaited<T>:解包 Promise 类型(TS 4.5+)==========
type PromiseString = Promise<string>
type Str = Awaited<PromiseString>  // string
type NestedPromise = Promise<Promise<number>>
type Num = Awaited<NestedPromise>  // number

💡 面试加分点: 面试经常会问"手写 Partial/Pick/Omit":

type MyPartial<T> = { [K in keyof T]?: T[K] }
type MyPick<T, K extends keyof T> = { [P in K]: T[P] }
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

7. 什么是条件类型(Conditional Types)?

// 基本语法:T extends U ? X : Y
type IsString<T> = T extends string ? 'yes' : 'no'
type A = IsString<string>   // 'yes'
type B = IsString<number>   // 'no'

// ========== infer 关键字:在条件类型中推断类型 ==========
// 提取数组元素类型
type ElementType<T> = T extends (infer E)[] ? E : never
type NumType = ElementType<number[]>  // number
type StrType = ElementType<string[]>  // string

// 提取函数返回值类型(手写 ReturnType)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type FnReturn = MyReturnType<() => string>  // string

// 提取 Promise 内部类型(手写 Awaited)
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T
type Result = UnwrapPromise<Promise<Promise<string>>>  // string

// ========== 分布式条件类型 ==========
// 当 T 是联合类型时,条件类型会分别对每个成员应用
type ToArray<T> = T extends any ? T[] : never
type StrOrNumArray = ToArray<string | number>
// string[] | number[](不是 (string | number)[])

// 避免分布式:用元组包裹
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never
type MixedArray = ToArrayNonDist<string | number>
// (string | number)[]

// ========== 实用示例:提取对象中值类型为 string 的键 ==========
type StringKeys<T> = {
  [K in keyof T]: T[K] extends string ? K : never
}[keyof T]

interface User {
  id: number
  name: string
  email: string
  age: number
}
type UserStringKeys = StringKeys<User>  // 'name' | 'email'

8. 什么是模板字面量类型(Template Literal Types)?

// 基本语法
type Greeting = `Hello, ${string}!`
let g: Greeting = 'Hello, World!'  // ✅
// let g2: Greeting = 'Hi, World!'   // ❌

// ========== 与联合类型组合 ==========
type Direction = 'top' | 'right' | 'bottom' | 'left'
type MarginProperty = `margin-${Direction}`
// 'margin-top' | 'margin-right' | 'margin-bottom' | 'margin-left'

type Size = 'sm' | 'md' | 'lg'
type Color = 'primary' | 'secondary' | 'danger'
type ButtonClass = `btn-${Size}-${Color}`
// 'btn-sm-primary' | 'btn-sm-secondary' | ... 共 9 种组合

// ========== 内置字符串工具类型 ==========
type Upper = Uppercase<'hello'>        // 'HELLO'
type Lower = Lowercase<'HELLO'>        // 'hello'
type Cap = Capitalize<'hello'>         // 'Hello'
type Uncap = Uncapitalize<'Hello'>     // 'hello'

// ========== 实用示例:事件处理器命名 ==========
type EventName = 'click' | 'focus' | 'blur'
type EventHandler = `on${Capitalize<EventName>}`
// 'onClick' | 'onFocus' | 'onBlur'

// ========== 实用示例:CSS-in-JS 属性类型 ==========
type CSSProperty = 'color' | 'background-color' | 'font-size'
type CamelCase<S extends string> =
  S extends `${infer F}-${infer R}`
    ? `${F}${Capitalize<CamelCase<R>>}`
    : S

type CSSPropertyCamel = CamelCase<CSSProperty>
// 'color' | 'backgroundColor' | 'fontSize'

9. TypeScript 中的 class 有哪些特性?

// ========== 访问修饰符 ==========
class Person {
  public name: string         // 公有(默认):任何地方都能访问
  protected age: number       // 受保护:自身和子类可以访问
  private _salary: number     // 私有:只有自身可以访问
  readonly id: number         // 只读:初始化后不可修改

  // 构造函数参数简写
  constructor(
    name: string,
    age: number,
    salary: number,
    readonly department: string  // 参数属性简写
  ) {
    this.name = name
    this.age = age
    this._salary = salary
    this.id = Math.random()
  }

  // getter / setter
  get salary(): number { return this._salary }
  set salary(value: number) {
    if (value < 0) throw new Error('薪资不能为负')
    this._salary = value
  }
}

// ========== 抽象类(Abstract Class)==========
abstract class Shape {
  abstract getArea(): number      // 抽象方法,子类必须实现
  abstract getPerimeter(): number

  // 非抽象方法可以有默认实现
  describe(): string {
    return `面积: ${this.getArea()}, 周长: ${this.getPerimeter()}`
  }
}

class Circle extends Shape {
  constructor(private radius: number) { super() }

  getArea(): number {
    return Math.PI * this.radius ** 2
  }
  getPerimeter(): number {
    return 2 * Math.PI * this.radius
  }
}

// const shape = new Shape()  // ❌ 不能实例化抽象类
const circle = new Circle(5)
circle.describe()  // '面积: 78.54, 周长: 31.42'

// ========== implements 实现接口 ==========
interface Serializable {
  serialize(): string
  deserialize(data: string): void
}

interface Loggable {
  log(): void
}

class User implements Serializable, Loggable {
  constructor(public name: string, public age: number) {}

  serialize(): string {
    return JSON.stringify({ name: this.name, age: this.age })
  }
  deserialize(data: string): void {
    const obj = JSON.parse(data)
    this.name = obj.name
    this.age = obj.age
  }
  log(): void {
    console.log(`User: ${this.name}, ${this.age}`)
  }
}

// ========== ES2022 私有字段(#)==========
class BankAccount {
  #balance: number = 0  // 真正的运行时私有

  deposit(amount: number) {
    this.#balance += amount
  }
  getBalance() {
    return this.#balance
  }
}
const account = new BankAccount()
// account.#balance  // ❌ 运行时也不能访问

💡 面试加分点: private 是 TS 编译时的私有(编译后消失),# 是 ES2022 运行时的私有(编译后依然存在)。抽象类和接口的区别:抽象类可以有默认实现,接口只能定义形状。


10. TypeScript 中的类型断言是什么?

// ========== as 语法(推荐)==========
const input = document.getElementById('username') as HTMLInputElement
input.value = '张三'  // 不需要额外判断

// ========== 尖括号语法(JSX 中不可用)==========
const input2 = <HTMLInputElement>document.getElementById('username')

// ========== 双重断言(慎用)==========
// 当 A 和 B 之间没有足够的关联性时
const value = 'hello' as unknown as number
// 先断言为 unknown,再断言为目标类型

// ========== const 断言 ==========
// 将值断言为最窄的字面量类型
const colors = ['red', 'green', 'blue'] as const
// 类型:readonly ['red', 'green', 'blue'](不是 string[])
type Color = typeof colors[number]  // 'red' | 'green' | 'blue'

const config = {
  endpoint: '/api',
  timeout: 3000,
  retries: 3
} as const
// 所有属性变成 readonly 且是字面量类型

// ========== 非空断言(!)==========
function getLength(str?: string) {
  // 你确定 str 一定有值时使用
  return str!.length  // 告诉编译器 str 不是 undefined
}

// ========== satisfies 操作符(TS 4.9+)==========
// 既检查类型,又保留推断的精确类型
type Route = {
  path: string
  component: string | (() => Promise<any>)
}

const routes = {
  home: { path: '/', component: 'HomePage' },
  about: { path: '/about', component: () => import('./About') }
} satisfies Record<string, Route>

// routes.home.component 的类型是 string(不是 string | (() => Promise<any>))
// 如果用 : Record<string, Route> 则会丢失精确类型

💡 面试加分点: as const 常用于定义常量配置和枚举替代方案;satisfies 是 TS 4.9 的重大特性,解决了类型注解和类型推断之间的矛盾。


11. TypeScript 中的映射类型(Mapped Types)?

// 基本语法:{ [K in Keys]: Type }
// 遍历键集合,生成新类型

// ========== 基础映射 ==========
type OptionsFlags<T> = {
  [K in keyof T]: boolean
}

interface Features {
  darkMode: () => void
  newProfile: () => void
}
type FeatureFlags = OptionsFlags<Features>
// { darkMode: boolean; newProfile: boolean }

// ========== 修饰符(+ / -)==========
// 添加只读
type ReadonlyAll<T> = {
  readonly [K in keyof T]: T[K]
}
// 移除只读(-readonly)
type Mutable<T> = {
  -readonly [K in keyof T]: T[K]
}
// 移除可选(-?)
type RequiredAll<T> = {
  [K in keyof T]-?: T[K]
}

// ========== 键重映射(as)—— TS 4.1+ ==========
// 给所有属性添加 get 前缀
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

interface Person {
  name: string
  age: number
}
type PersonGetters = Getters<Person>
// { getName: () => string; getAge: () => number }

// 过滤特定属性
type RemoveFunctions<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K]
}

interface Mixed {
  name: string
  age: number
  greet: () => void
}
type DataOnly = RemoveFunctions<Mixed>
// { name: string; age: number }

// ========== 实用示例:表单类型 ==========
interface FormData {
  username: string
  email: string
  age: number
}

// 自动生成表单错误类型
type FormErrors<T> = {
  [K in keyof T]?: string
}

// 自动生成表单 touched 状态
type FormTouched<T> = {
  [K in keyof T]?: boolean
}

type UserFormErrors = FormErrors<FormData>
// { username?: string; email?: string; age?: string }

12. TypeScript 中的装饰器(Decorators)?

// 装饰器是一种特殊的声明,可以附加到类、方法、属性或参数上
// 需要在 tsconfig.json 中开启 experimentalDecorators

// ========== 类装饰器 ==========
function Logger(logString: string) {
  return function <T extends { new (...args: any[]): {} }>(constructor: T) {
    console.log(logString)
    return class extends constructor {
      // 可以扩展或修改类
    }
  }
}

@Logger('正在创建 User 实例...')
class User {
  constructor(public name: string) {}
}

// ========== 方法装饰器 ==========
function Log(target: any, propertyName: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value

  descriptor.value = function (...args: any[]) {
    console.log(`调用 ${propertyName},参数:`, args)
    const result = originalMethod.apply(this, args)
    console.log(`${propertyName} 返回:`, result)
    return result
  }
}

class Calculator {
  @Log
  add(a: number, b: number) {
    return a + b
  }
}

const calc = new Calculator()
calc.add(2, 3)
// 输出:调用 add,参数: [2, 3]
// 输出:add 返回: 5

// ========== 属性装饰器:自动验证 ==========
function MinLength(min: number) {
  return function (target: any, propertyName: string) {
    let value: string

    Object.defineProperty(target, propertyName, {
      get() { return value },
      set(newVal: string) {
        if (newVal.length < min) {
          throw new Error(`${propertyName} 最少 ${min} 个字符`)
        }
        value = newVal
      }
    })
  }
}

class Form {
  @MinLength(3)
  username!: string
}

// ========== TC39 Stage 3 装饰器(TS 5.0+)==========
// 新语法,不需要 experimentalDecorators
function logged(value: Function, context: ClassMethodDecoratorContext) {
  const methodName = String(context.name)
  function replacementMethod(this: any, ...args: any[]) {
    console.log(`调用 ${methodName}`)
    return value.call(this, ...args)
  }
  return replacementMethod
}

💡 面试加分点: 装饰器在 NestJS、Angular 中大量使用。TS 5.0 引入了 TC39 Stage 3 的标准装饰器语法,与旧版 experimentalDecorators 不兼容。


13. TypeScript 中的模块与命名空间?

// ========== ES Modules(推荐)==========
// utils.ts
export function formatDate(date: Date): string {
  return date.toISOString()
}
export interface DateFormatOptions {
  locale?: string
  format?: string
}

// 默认导出
export default class DateFormatter {
  format(date: Date) { return date.toLocaleDateString() }
}

// main.ts
import DateFormatter, { formatDate, type DateFormatOptions } from './utils'
// TS 4.5+:type-only import
import type { DateFormatOptions } from './utils'

// ========== 命名空间(Namespace)==========
// 适合在没有模块系统的环境中组织代码
namespace Validation {
  export interface StringValidator {
    isValid(s: string): boolean
  }

  export class EmailValidator implements StringValidator {
    isValid(s: string) {
      return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s)
    }
  }

  export class PhoneValidator implements StringValidator {
    isValid(s: string) {
      return /^1[3-9]\d{9}$/.test(s)
    }
  }
}

const emailValidator = new Validation.EmailValidator()
emailValidator.isValid('test@example.com')  // true

// ========== 声明文件(.d.ts)==========
// 为没有 TS 类型的第三方库提供类型
// global.d.ts
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

declare module '*.css' {
  const classes: Record<string, string>
  export default classes
}

// 扩展已有类型
declare global {
  interface Window {
    __APP_VERSION__: string
  }
}

// ========== 三斜线指令 ==========
/// <reference types="vite/client" />
/// <reference path="./types.d.ts" />

14. tsconfig.json 常用配置解析?

{
  "compilerOptions": {
    // ===== 基础选项 =====
    "target": "ES2020",           // 编译目标版本
    "module": "ESNext",           // 模块系统
    "lib": ["ES2020", "DOM"],     // 包含的类型声明库
    "outDir": "./dist",           // 输出目录
    "rootDir": "./src",           // 源码根目录

    // ===== 严格模式(推荐全部开启)=====
    "strict": true,               // 开启所有严格检查(等于下面所有 true)
    // "noImplicitAny": true,     // 禁止隐式 any
    // "strictNullChecks": true,  // 严格 null 检查
    // "strictFunctionTypes": true, // 严格函数类型
    // "strictBindCallApply": true, // 严格 bind/call/apply
    // "noImplicitThis": true,     // 禁止隐式 this
    // "alwaysStrict": true,       // 总是 use strict

    // ===== 模块解析 =====
    "moduleResolution": "bundler",  // 模块解析策略(TS 5.0+)
    "esModuleInterop": true,       // 允许 import x from 'commonjs-module'
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,      // 允许 import json 文件
    "baseUrl": "./",
    "paths": {                      // 路径别名
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"]
    },

    // ===== 类型检查 =====
    "skipLibCheck": true,           // 跳过 .d.ts 文件的类型检查(加速编译)
    "forceConsistentCasingInFileNames": true,  // 强制文件名大小写一致
    "noUnusedLocals": true,         // 不允许未使用的局部变量
    "noUnusedParameters": true,     // 不允许未使用的参数
    "noFallthroughCasesInSwitch": true,  // switch 必须有 break

    // ===== JSX =====
    "jsx": "react-jsx",             // React 17+ JSX 转换
    // "jsx": "preserve",           // Vue 项目保留 JSX

    // ===== 装饰器 =====
    "experimentalDecorators": true,  // 旧版装饰器
    "emitDecoratorMetadata": true,   // 装饰器元数据

    // ===== 输出 =====
    "declaration": true,             // 生成 .d.ts 类型声明
    "declarationMap": true,          // 生成 .d.ts.map
    "sourceMap": true,               // 生成 .js.map
    "noEmit": true                   // 不输出文件(由 Vite/Webpack 处理)
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

💡 面试加分点: 新项目建议用 "moduleResolution": "bundler"(TS 5.0+),它更好地匹配 Vite/Webpack 的模块解析行为。"strict": true 一定要开——关闭严格模式几乎等于放弃 TS 的核心价值。


15. TypeScript 中的协变与逆变?

// 协变(Covariance):子类型可以赋值给父类型
// 逆变(Contravariance):父类型可以赋值给子类型(仅函数参数)

class Animal { name = 'animal' }
class Dog extends Animal { breed = 'husky' }
class Greyhound extends Dog { speed = 100 }

// ========== 协变:返回值类型 ==========
// Dog 是 Animal 的子类型
// () => Dog 是 () => Animal 的子类型(协变)
type AnimalFactory = () => Animal
type DogFactory = () => Dog

const makeDog: DogFactory = () => new Dog()
const makeAnimal: AnimalFactory = makeDog  // ✅ 协变

// ========== 逆变:参数类型 ==========
// (animal: Animal) => void 是 (dog: Dog) => void 的子类型(逆变)
type AnimalHandler = (animal: Animal) => void
type DogHandler = (dog: Dog) => void

const handleAnimal: AnimalHandler = (animal) => console.log(animal.name)
const handleDog: DogHandler = handleAnimal  // ✅ 逆变

// ========== 实际场景 ==========
// 数组是协变的
const dogs: Dog[] = [new Dog()]
const animals: Animal[] = dogs  // ✅(但有潜在风险)

// 函数参数在严格模式下是逆变的(strictFunctionTypes: true)
interface Comparer<T> {
  compare: (a: T, b: T) => number
}
let animalComparer: Comparer<Animal> = {
  compare: (a, b) => a.name.localeCompare(b.name)
}
let dogComparer: Comparer<Dog>
// dogComparer = animalComparer  // ✅ 严格模式下:逆变

💡 面试加分点: 简单记忆——返回值协变(子→父),参数逆变(父→子)。开启 strictFunctionTypes 后函数参数才是逆变的,否则是双变(既协变又逆变)。


16. TypeScript 在 React 中的实践?

// ========== 函数组件 ==========
interface UserCardProps {
  name: string
  age: number
  avatar?: string
  onEdit: (id: number) => void
  children?: React.ReactNode
}

const UserCard: React.FC<UserCardProps> = ({ name, age, avatar, onEdit, children }) => {
  return (
    <div>
      <h2>{name}</h2>
      <p>年龄: {age}</p>
      {avatar && <img src={avatar} alt={name} />}
      <button onClick={() => onEdit(1)}>编辑</button>
      {children}
    </div>
  )
}

// 推荐写法(不用 React.FC)
function UserCard2({ name, age }: UserCardProps) {
  return <div>{name} - {age}</div>
}

// ========== Hooks 类型 ==========
// useState
const [count, setCount] = useState<number>(0)
const [user, setUser] = useState<User | null>(null)

// useRef
const inputRef = useRef<HTMLInputElement>(null)
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)

// useReducer
type State = { count: number; loading: boolean }
type Action =
  | { type: 'increment'; payload: number }
  | { type: 'decrement'; payload: number }
  | { type: 'setLoading'; payload: boolean }

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment': return { ...state, count: state.count + action.payload }
    case 'decrement': return { ...state, count: state.count - action.payload }
    case 'setLoading': return { ...state, loading: action.payload }
  }
}
const [state, dispatch] = useReducer(reducer, { count: 0, loading: false })

// ========== 事件类型 ==========
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => { }
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { }
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { }
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { }

// ========== 泛型组件 ==========
interface ListProps<T> {
  items: T[]
  renderItem: (item: T, index: number) => React.ReactNode
  keyExtractor: (item: T) => string
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={keyExtractor(item)}>{renderItem(item, index)}</li>
      ))}
    </ul>
  )
}

// 使用泛型组件
<List
  items={[{ id: 1, name: '张三' }]}
  renderItem={(item) => <span>{item.name}</span>}
  keyExtractor={(item) => String(item.id)}
/>

17. TypeScript 在 Vue3 中的实践?

// ========== 组合式 API + TypeScript ==========
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'

// ref 类型推断
const count = ref(0)              // Ref<number>
const user = ref<User | null>(null)  // 需要显式指定

// reactive
import { reactive } from 'vue'
interface FormState {
  username: string
  password: string
  remember: boolean
}
const form = reactive<FormState>({
  username: '',
  password: '',
  remember: false
})

// computed
const doubleCount = computed(() => count.value * 2)  // ComputedRef<number>

// ========== defineProps 类型声明 ==========
// 方式1:运行时声明
const props = defineProps({
  title: { type: String, required: true },
  count: { type: Number, default: 0 }
})

// 方式2:类型声明(推荐)
interface Props {
  title: string
  count?: number
  items: string[]
}
const props = defineProps<Props>()

// 带默认值(使用 withDefaults)
const props = withDefaults(defineProps<Props>(), {
  count: 0,
  items: () => []
})

// ========== defineEmits 类型声明 ==========
const emit = defineEmits<{
  (e: 'update', value: string): void
  (e: 'delete', id: number): void
  (e: 'submit'): void
}>()

// Vue 3.3+ 更简洁的写法
const emit = defineEmits<{
  update: [value: string]
  delete: [id: number]
  submit: []
}>()

emit('update', '新值')
emit('delete', 1)

// ========== Provide / Inject 类型安全 ==========
import { provide, inject, type InjectionKey } from 'vue'

interface UserContext {
  user: Ref<User | null>
  login: (name: string) => void
  logout: () => void
}

const UserKey: InjectionKey<UserContext> = Symbol('user')

// 父组件
provide(UserKey, {
  user: ref(null),
  login: (name) => { /* ... */ },
  logout: () => { /* ... */ }
})

// 子组件
const userCtx = inject(UserKey)!  // UserContext 类型
</script>

18. 手写常见类型体操题?

// ========== 1. 实现 DeepReadonly ==========
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepReadonly<T[K]>
    : T[K]
}

// ========== 2. 实现 DeepPartial ==========
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepPartial<T[K]>
    : T[K]
}

// ========== 3. 实现 TupleToUnion ==========
type TupleToUnion<T extends readonly any[]> = T[number]
type Result = TupleToUnion<[string, number, boolean]>  // string | number | boolean

// ========== 4. 实现 Last(获取元组最后一个元素类型)==========
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never
type LastItem = Last<[1, 2, 3]>  // 3

// ========== 5. 实现 Trim(去除字符串类型两端空格)==========
type TrimLeft<S extends string> = S extends ` ${infer R}` ? TrimLeft<R> : S
type TrimRight<S extends string> = S extends `${infer L} ` ? TrimRight<L> : S
type Trim<S extends string> = TrimRight<TrimLeft<S>>
type Trimmed = Trim<'  hello  '>  // 'hello'

// ========== 6. 实现 Flatten ==========
type Flatten<T extends any[]> = T extends [infer F, ...infer R]
  ? F extends any[]
    ? [...Flatten<F>, ...Flatten<R>]
    : [F, ...Flatten<R>]
  : T
type Flat = Flatten<[1, [2, [3, 4]], 5]>  // [1, 2, 3, 4, 5]

// ========== 7. 实现 PickByType ==========
type PickByType<T, V> = {
  [K in keyof T as T[K] extends V ? K : never]: T[K]
}
interface Model {
  name: string
  count: number
  isActive: boolean
  label: string
}
type StringProps = PickByType<Model, string>
// { name: string; label: string }

💡 面试加分点: 类型体操虽然面试常考,但实际工作中不建议写过于复杂的类型(会降低可读性)。重点掌握 infer、递归类型、映射类型、条件类型四大核心技巧即可。


19. TypeScript 的类型兼容性规则?

// TypeScript 使用结构类型系统(Structural Typing)
// 即鸭子类型:只要结构匹配就兼容,不要求显式声明继承关系

interface Point2D { x: number; y: number }
interface Point3D { x: number; y: number; z: number }

let p2d: Point2D = { x: 1, y: 2 }
let p3d: Point3D = { x: 1, y: 2, z: 3 }

p2d = p3d  // ✅ Point3D 有 Point2D 的所有属性(多的可以赋给少的)
// p3d = p2d  // ❌ Point2D 缺少 z 属性

// ========== 对象字面量额外属性检查 ==========
// 直接赋值对象字面量时,TS 会做额外属性检查
interface Config {
  width: number
  color?: string
}

// const cfg: Config = { width: 100, opacity: 0.5 }  // ❌ 额外属性检查
const temp = { width: 100, opacity: 0.5 }
const cfg: Config = temp  // ✅ 通过中间变量绕过额外属性检查

// ========== 函数兼容性 ==========
// 参数少的可以赋值给参数多的(回调函数常见)
type Handler = (a: string, b: number) => void
const fn: Handler = (a) => console.log(a)  // ✅ 少参数可以赋给多参数

// 场景:Array.forEach 的回调只需要用到 item
[1, 2, 3].forEach((item) => console.log(item))
// forEach 的回调签名是 (value, index, array),但我们可以只接收 value

// ========== 枚举兼容性 ==========
enum Status { Active, Inactive }
enum Color { Red, Blue }
// let s: Status = Color.Red  // ❌ 不同枚举类型不兼容
let s: Status = 0  // ✅ 数字枚举与 number 兼容

20. TypeScript 常见报错及解决方案?

// ========== 1. "对象可能为 undefined" ==========
// 错误示例
function getUser(): User | undefined { /* ... */ }
const user = getUser()
// console.log(user.name)  // ❌ 对象可能为 "undefined"

// 解决方案
// 方案1:可选链
console.log(user?.name)
// 方案2:类型守卫
if (user) { console.log(user.name) }
// 方案3:非空断言(确定不为空时)
console.log(user!.name)

// ========== 2. "不能将类型 X 分配给类型 Y" ==========
// 常见于联合类型未收窄
function handle(input: string | number) {
  // input.toFixed()  // ❌ string 没有 toFixed
  if (typeof input === 'number') {
    input.toFixed()   // ✅ 收窄为 number
  }
}

// ========== 3. "类型 X 上不存在属性 Y" ==========
// 场景:访问动态属性
const obj: Record<string, any> = {}
obj.dynamicProp  // ✅ 用 Record 或 索引签名

interface DynamicObj {
  [key: string]: unknown   // 索引签名
  knownProp: string        // 已知属性
}

// ========== 4. "隐式具有 any 类型" ==========
// tsconfig 开启 noImplicitAny 后
// function fn(x) {}  // ❌ 参数 x 隐式具有 any 类型
function fn(x: unknown) {}  // ✅

// ========== 5. "找不到模块 X 的声明文件" ==========
// 方案1:安装 @types 包
// npm install @types/lodash --save-dev

// 方案2:手动声明
// declare module 'some-untyped-lib' {
//   const lib: any
//   export default lib
// }

// 方案3:在 d.ts 文件中声明
// shims.d.ts
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

// ========== 6. 类型断言解决复杂场景 ==========
// 当你比编译器更了解类型时
const canvas = document.getElementById('canvas') as HTMLCanvasElement
const ctx = canvas.getContext('2d')!  // 非空断言

💡 面试加分点: 遇到类型报错的正确处理顺序:1)类型收窄(推荐)→ 2)类型守卫 → 3)类型断言(慎用)→ 4)@ts-ignore(最后手段,极不推荐)。