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:没有返回值(但函数会正常结束)
}
💡 面试加分点:
any和unknown的核心区别——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 对比:
| 特性 | interface | type |
|---|---|---|
| 对象形状 | ✅ | ✅ |
| 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(最后手段,极不推荐)。