TypeScript 完全指南

相关文档:JavaScript入门(/javascript-ru-men/) | Vue3入门(/vue-3-ru-men-zhi-nan/) | JS模块系统(/js-mjs-ts-wen-jian-qu-bie-yu-zui-jia-shi-jian/) 1. 基础类型系统(#%E4%B8%80%E3%80%81%E5%9F%BA%E7%A1%80%E7%B1%BB%E5%9E%8B%E7%B3%BB%E7%BB%9F) 2. 函数类型(#%E4%BA%8C%E3%80%81%E5%87%BD%E6%95%B0%E7%B1%BB%E5%9E%8B)

分享

官方文档:https://www.typescriptlang.org/docs/
TypeScript Playground:https://www.typescriptlang.org/play | tsconfig 参考:https://www.typescriptlang.org/tsconfig
适用版本:TypeScript 5.x(2026-05-07 核实)

相关文档:JavaScript入门 | Vue3入门 | JS模块系统


目录

  1. 基础类型系统
  2. 函数类型
  3. 接口与类型别名
  4. 泛型
  5. 高级类型
  6. 模块与命名空间
  7. 装饰器
  8. 类型声明文件
  9. tsconfig.json 完整配置
  10. Vue 3 + TypeScript 最佳实践
  11. 项目文件结构
  12. 最佳实践
  13. 常见陷阱与注意事项

一、基础类型系统

原始类型

// 基本类型注解
let name: string = 'Alice'
let age: number = 25
let active: boolean = true
let big: bigint = 100n
let sym: symbol = Symbol('key')

// 空值类型
let nothing: null = null
let missing: undefined = undefined
let voidFn: void = undefined   // 通常用于函数无返回值

// 顶层类型
let anything: any = '可以是任何值,跳过类型检查'
let unknown: unknown = getData()  // 比 any 安全,使用前需收窄

// 底层类型
function throwError(msg: string): never {
    throw new Error(msg)          // never:永不返回的函数
}

// object 类型(非原始类型)
let obj: object = { a: 1 }

数组与元组

// 数组(两种等价写法)
let nums: number[] = [1, 2, 3]
let strs: Array<string> = ['a', 'b', 'c']

// 只读数组
let frozen: readonly number[] = [1, 2, 3]
let frozen2: ReadonlyArray<number> = [1, 2, 3]
frozen.push(4)   // Error: Property 'push' does not exist on type 'readonly number[]'

// 元组:固定长度和类型的数组
let pair: [string, number] = ['Alice', 25]
let triple: [string, number, boolean] = ['Bob', 30, true]

// 可选元素元组
let optional: [string, number?] = ['Alice']

// 剩余元素元组
let rest: [string, ...number[]] = ['Alice', 1, 2, 3]

// 具名元组(TypeScript 4.0+)
let named: [name: string, age: number] = ['Alice', 25]

字面量类型

// 字符串字面量
let direction: 'left' | 'right' | 'up' | 'down' = 'left'

// 数字字面量
let statusCode: 200 | 201 | 400 | 404 | 500 = 200

// 布尔字面量
let alwaysTrue: true = true

// 字面量推断问题
const config = { method: 'GET' }          // 推断为 { method: string }
const config2 = { method: 'GET' } as const // 推断为 { readonly method: "GET" }

// 模板字面量类型(TypeScript 4.1+)
type EventName = `on${Capitalize<string>}`
type CSSUnit = `${number}${'px' | 'rem' | 'em' | '%'}`
type Width = `${number}px`
let w: Width = '100px'

类型推断

// TypeScript 会自动推断类型,大多数情况无需显式注解
let x = 42                    // 推断为 number
let y = 'hello'               // 推断为 string
let z = [1, 2, 3]             // 推断为 number[]
let fn = (a: number) => a * 2 // 返回值推断为 number

// 上下文推断
window.addEventListener('click', (e) => {
    // e 被推断为 MouseEvent(TypeScript 知道 click 事件的类型)
    console.log(e.clientX)
})

类型断言

// as 语法(推荐)
const input = document.getElementById('username') as HTMLInputElement
const value = input.value

// 尖括号语法(不能在 .tsx 文件中使用)
const input2 = <HTMLInputElement>document.getElementById('username')

// 双重断言(先断言为 unknown,再断言为目标类型)
// 仅在确实无法直接断言时使用
const x = 'hello' as unknown as number   // 不推荐,除非必要

// satisfies 运算符(TypeScript 4.9+)
// 验证类型而不改变推断结果
const palette = {
    red: [255, 0, 0],
    green: '#00ff00',
} satisfies Record<string, string | number[]>

palette.red.map(x => x)       // 正确:red 仍推断为 number[]
palette.green.toUpperCase()   // 正确:green 仍推断为 string

二、函数类型

函数声明与注解

// 参数类型 + 返回值类型
function add(a: number, b: number): number {
    return a + b
}

// 可选参数(必须在必填参数之后)
function greet(name: string, greeting?: string): string {
    return `${greeting ?? 'Hello'}, ${name}!`
}

// 默认参数
function createUser(name: string, role: string = 'user'): object {
    return { name, role }
}

// 剩余参数
function sum(...nums: number[]): number {
    return nums.reduce((acc, n) => acc + n, 0)
}

// 箭头函数
const multiply = (a: number, b: number): number => a * b

// 函数类型表达式
type BinaryOp = (a: number, b: number) => number
const add2: BinaryOp = (a, b) => a + b

函数重载

// 重载签名(只有声明,无实现)
function process(input: string): string
function process(input: number): number
function process(input: string[]): string

// 实现签名(处理所有情况,参数/返回类型足够宽泛)
function process(input: string | number | string[]): string | number {
    if (typeof input === 'string') return input.toUpperCase()
    if (typeof input === 'number') return input * 2
    return input.join(', ')
}

// 调用时 TypeScript 匹配重载签名
const a = process('hello')    // 类型为 string
const b = process(42)         // 类型为 number

this 类型

// 显式声明 this 的类型(TypeScript 的伪参数,不计入实际参数)
function onClick(this: HTMLButtonElement, event: MouseEvent): void {
    console.log(this.textContent)
}

// 对象方法中的 this
interface Counter {
    count: number
    increment(this: Counter): void
}

函数类型工具

// Parameters<T>:获取函数参数类型元组
function fetchUser(id: number, token: string): Promise<User> { ... }
type FetchParams = Parameters<typeof fetchUser>   // [id: number, token: string]

// ReturnType<T>:获取函数返回类型
type FetchResult = ReturnType<typeof fetchUser>   // Promise<User>

// ConstructorParameters<T>:获取构造函数参数类型
class User { constructor(name: string, age: number) {} }
type UserArgs = ConstructorParameters<typeof User>   // [name: string, age: number]

三、接口与类型别名

interface

// 基础接口
interface User {
    readonly id: number          // 只读
    name: string
    email?: string               // 可选
    createdAt: Date
}

// 方法签名
interface Repository<T> {
    findById(id: number): Promise<T>
    findAll(): Promise<T[]>
    save(entity: T): Promise<T>
    delete(id: number): Promise<void>
}

// 索引签名
interface StringMap {
    [key: string]: string        // 任意字符串键,值为 string
}

interface NumberArray {
    [index: number]: number      // 数字索引,值为 number
}

// 接口继承(支持多继承)
interface Animal {
    name: string
    eat(): void
}

interface Pet {
    owner: string
}

interface Dog extends Animal, Pet {
    breed: string
    bark(): void
}

// 接口合并(同名接口自动合并,重要特性)
interface Window {
    myGlobal: string
}
interface Window {
    anotherGlobal: number
}
// 结果:Window 同时拥有 myGlobal 和 anotherGlobal

type 类型别名

// 基础别名
type ID = string | number
type Nullable<T> = T | null
type Callback = () => void

// 对象类型
type Point = {
    x: number
    y: number
}

// 交叉类型(合并多个类型)
type AdminUser = User & { role: 'admin'; permissions: string[] }

// 联合类型
type StringOrNumber = string | number
type Status = 'pending' | 'active' | 'inactive'

// 条件类型(见高级类型章节)
type IsString<T> = T extends string ? true : false

// 映射类型(见高级类型章节)
type Optional<T> = { [K in keyof T]?: T[K] }

interface vs type 选择原则

场景 推荐 原因
定义对象/类的结构 interface 可被继承、可合并扩展
描述函数签名 typeinterface 均可 风格统一即可
联合类型、元组类型 type interface 不支持
交叉类型 type interface 的继承有限制
需要声明合并(扩展第三方库) interface type 不支持同名合并
工具类型(映射/条件类型) type interface 不支持

四、类

基本语法

class Animal {
    // 属性声明(TypeScript 要求先声明)
    name: string
    private age: number
    protected species: string
    readonly id: number
    static count: number = 0

    constructor(name: string, age: number) {
        this.name = name
        this.age = age
        this.species = 'Unknown'
        this.id = Math.random()
        Animal.count++
    }

    // 实例方法
    speak(): string {
        return `${this.name} makes a sound`
    }

    // getter / setter
    get info(): string {
        return `${this.name} (${this.age})`
    }

    set nickname(value: string) {
        this.name = value
    }

    // 静态方法
    static getCount(): number {
        return Animal.count
    }
}

访问修饰符

修饰符 类内部 子类 类外部 说明
public(默认) 可访问 可访问 可访问 公开
protected 可访问 可访问 不可访问 类及子类内部
private 可访问 不可访问 不可访问 仅类内部
#privateField(JS私有) 可访问 不可访问 不可访问 运行时真正私有
readonly 只能在构造函数赋值 只读 不可修改
class BankAccount {
    readonly id: string
    private balance: number
    #pin: number                          // JS 原生私有字段(运行时私有)

    constructor(initialBalance: number, pin: number) {
        this.id = crypto.randomUUID()
        this.balance = initialBalance
        this.#pin = pin
    }

    withdraw(amount: number, pin: number): boolean {
        if (pin !== this.#pin) return false
        if (amount > this.balance) return false
        this.balance -= amount
        return true
    }
}

构造函数参数简写

// 不简写(冗余)
class User {
    public name: string
    private age: number
    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }
}

// 简写(在参数前加修饰符,自动声明并赋值)
class User {
    constructor(
        public name: string,
        private age: number,
        protected readonly role: string = 'user'
    ) {}
}

继承与抽象类

// 抽象类:不能直接实例化,用于定义基类契约
abstract class Shape {
    abstract getArea(): number          // 抽象方法:子类必须实现
    abstract getPerimeter(): number

    // 具体方法可以有实现
    describe(): string {
        return `Area: ${this.getArea()}, Perimeter: ${this.getPerimeter()}`
    }
}

class Circle extends Shape {
    constructor(private radius: number) {
        super()                          // 必须调用 super()
    }

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

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

class Rectangle extends Shape {
    constructor(private width: number, private height: number) {
        super()
    }

    getArea(): number {
        return this.width * this.height
    }

    getPerimeter(): number {
        return 2 * (this.width + this.height)
    }
}

实现接口

interface Serializable {
    serialize(): string
    deserialize(data: string): this
}

interface Validatable {
    validate(): boolean
    errors: string[]
}

// 一个类可以实现多个接口
class UserModel implements Serializable, Validatable {
    errors: string[] = []

    constructor(public name: string, public email: string) {}

    serialize(): string {
        return JSON.stringify({ name: this.name, email: this.email })
    }

    deserialize(data: string): this {
        const parsed = JSON.parse(data)
        Object.assign(this, parsed)
        return this
    }

    validate(): boolean {
        this.errors = []
        if (!this.name) this.errors.push('Name is required')
        if (!this.email.includes('@')) this.errors.push('Invalid email')
        return this.errors.length === 0
    }
}

五、泛型

泛型函数

// 基础泛型
function identity<T>(value: T): T {
    return value
}

const str = identity<string>('hello')   // 显式指定
const num = identity(42)                // 自动推断为 number

// 多个类型参数
function pair<T, U>(first: T, second: U): [T, U] {
    return [first, second]
}

// 泛型约束
function getLength<T extends { length: number }>(arg: T): number {
    return arg.length
}

getLength('hello')        // 5
getLength([1, 2, 3])      // 3
getLength(42)             // Error: number 没有 length 属性

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

const user = { name: 'Alice', age: 25 }
const name = getProperty(user, 'name')   // 类型为 string
const age = getProperty(user, 'age')     // 类型为 number
getProperty(user, 'email')               // Error: 'email' 不在 keyof 中

泛型接口与类

// 泛型接口
interface ApiResponse<T> {
    data: T
    status: number
    message: string
    timestamp: Date
}

// 泛型类
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
    }

    isEmpty(): boolean {
        return this.items.length === 0
    }
}

const numStack = new Stack<number>()
numStack.push(1)
numStack.push(2)

泛型默认值

// 泛型参数可以有默认值
interface Container<T = string> {
    value: T
    label: string
}

const c1: Container = { value: 'hello', label: 'text' }      // T 默认为 string
const c2: Container<number> = { value: 42, label: 'number' }  // 显式指定

条件类型中的泛型推断(infer)

// infer:在条件类型中推断类型
type UnpackPromise<T> = T extends Promise<infer U> ? U : T

type A = UnpackPromise<Promise<string>>   // string
type B = UnpackPromise<number>            // number

// 获取函数返回类型(内置 ReturnType 的实现原理)
type MyReturnType<T extends (...args: any) => any> =
    T extends (...args: any) => infer R ? R : never

// 获取数组元素类型
type ElementType<T> = T extends (infer U)[] ? U : never
type E = ElementType<string[]>   // string

六、高级类型

联合类型与交叉类型

// 联合类型:A 或 B
type StringOrNumber = string | number

// 判别联合类型(Discriminated Unions)—— 非常重要的模式
type Shape =
    | { kind: 'circle'; radius: number }
    | { kind: 'square'; side: number }
    | { kind: 'rectangle'; width: number; height: number }

function getArea(shape: Shape): number {
    switch (shape.kind) {
        case 'circle':
            return Math.PI * shape.radius ** 2   // shape 收窄为 circle
        case 'square':
            return shape.side ** 2               // shape 收窄为 square
        case 'rectangle':
            return shape.width * shape.height    // shape 收窄为 rectangle
    }
}

// 交叉类型:A 且 B
type WithTimestamp<T> = T & { createdAt: Date; updatedAt: Date }
type UserWithTimestamp = WithTimestamp<User>

映射类型

// 基础映射类型
type Readonly<T> = {
    readonly [K in keyof T]: T[K]
}

type Partial<T> = {
    [K in keyof T]?: T[K]
}

type Required<T> = {
    [K in keyof T]-?: T[K]    // -? 移除可选修饰符
}

// 自定义映射类型:让所有属性变为 nullable
type Nullable<T> = {
    [K in keyof T]: T[K] | null
}

// 键重映射(TypeScript 4.1+,使用 as 子句)
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

interface User {
    name: string
    age: number
}

type UserGetters = Getters<User>
// { getName: () => string; getAge: () => number }

条件类型

// 基础条件类型
type IsArray<T> = T extends any[] ? true : false

type A = IsArray<string[]>   // true
type B = IsArray<string>     // false

// 分布式条件类型(当 T 为联合类型时,分别应用)
type ToArray<T> = T extends any ? T[] : never

type C = ToArray<string | number>   // string[] | number[](分布式)

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

// 内置条件工具类型
type E = Exclude<'a' | 'b' | 'c', 'a'>          // 'b' | 'c'
type F = Extract<'a' | 'b' | 'c', 'a' | 'd'>    // 'a'
type G = NonNullable<string | null | undefined>  // string

索引访问类型

interface User {
    name: string
    address: {
        city: string
        country: string
    }
    tags: string[]
}

type NameType = User['name']              // string
type AddressType = User['address']        // { city: string; country: string }
type CityType = User['address']['city']   // string
type TagType = User['tags'][number]       // string(数组元素类型)

// 结合 keyof 使用
type UserValues = User[keyof User]        // string | { city: string; country: string } | string[]

模板字面量类型

type EventName = 'click' | 'focus' | 'blur'
type HandlerName = `on${Capitalize<EventName>}`
// 'onClick' | 'onFocus' | 'onBlur'

// 实际应用:CSS 属性值类型
type LengthUnit = 'px' | 'rem' | 'em' | '%' | 'vh' | 'vw'
type CSSLength = `${number}${LengthUnit}`

// 实际应用:API 路径类型
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE'
type Endpoint = '/users' | '/posts' | '/comments'
type Route = `${Method} ${Endpoint}`
// 'GET /users' | 'GET /posts' | ... 所有组合

内置实用工具类型完整参考

interface User {
    id: number
    name: string
    email: string
    age?: number
    readonly createdAt: Date
}
工具类型 语法 效果
Partial<T> Partial<User> 所有属性变可选
Required<T> Required<User> 所有属性变必填
Readonly<T> Readonly<User> 所有属性只读
Pick<T, K> Pick<User, 'id' | 'name'> 选取指定属性
Omit<T, K> Omit<User, 'email'> 排除指定属性
Record<K, V> Record<string, User> 构造键值类型
Exclude<T, U> Exclude<string | number, number> 联合中排除
Extract<T, U> Extract<string | number, number> 联合中提取
NonNullable<T> NonNullable<string | null> 排除 null/undefined
ReturnType<T> ReturnType<typeof fn> 函数返回类型
Parameters<T> Parameters<typeof fn> 函数参数类型元组
ConstructorParameters<T> ConstructorParameters<typeof MyClass> 构造函数参数类型
InstanceType<T> InstanceType<typeof MyClass> 类的实例类型
Awaited<T> Awaited<Promise<string>> 解包 Promise(4.5+)
NoInfer<T> NoInfer<T> 禁止从该位置推断(5.4+)

类型守卫

// typeof 守卫
function process(value: string | number): string {
    if (typeof value === 'string') {
        return value.toUpperCase()   // value: string
    }
    return value.toFixed(2)          // value: number
}

// instanceof 守卫
function handleError(err: unknown): string {
    if (err instanceof Error) {
        return err.message           // err: Error
    }
    return String(err)
}

// in 守卫(检查属性是否存在)
interface Cat { meow(): void }
interface Dog { bark(): void }

function makeSound(animal: Cat | Dog): void {
    if ('meow' in animal) {
        animal.meow()               // animal: Cat
    } else {
        animal.bark()               // animal: Dog
    }
}

// 自定义类型谓词(user-defined type guards)
function isUser(value: unknown): value is User {
    return (
        typeof value === 'object' &&
        value !== null &&
        'id' in value &&
        'name' in value
    )
}

// 断言函数(TypeScript 3.7+)
function assertIsString(value: unknown): asserts value is string {
    if (typeof value !== 'string') {
        throw new TypeError(`Expected string, got ${typeof value}`)
    }
}

// 使用
const input: unknown = getInput()
assertIsString(input)
console.log(input.toUpperCase())   // 这里 input 已被收窄为 string

七、模块与命名空间

ES 模块(推荐)

// 导出
export interface User { id: number; name: string }
export type Status = 'active' | 'inactive'
export const DEFAULT_ROLE = 'user'
export function createUser(name: string): User { ... }
export class UserService { ... }

// 默认导出
export default class ApiClient { ... }

// 重新导出
export { createUser as makeUser } from './user'
export * from './helpers'
export * as utils from './utils'

// 导入
import ApiClient from './api-client'
import { User, createUser, DEFAULT_ROLE } from './user'
import type { User } from './user'   // 仅导入类型(不产生运行时代码)
import * as utils from './utils'

import type 的重要性

// 普通 import:TypeScript 可能保留运行时导入
import { User } from './user'

// import type:编译后完全消除,确保不产生运行时依赖
import type { User } from './user'
import type { Ref, ComputedRef } from 'vue'

// 内联 type 导入(TypeScript 4.5+)
import { type User, createUser } from './user'

命名空间(namespace)

// 命名空间(主要用于组织声明文件,现代项目优先用模块)
namespace Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean
    }

    export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string): boolean {
            return /^[A-Za-z]+$/.test(s)
        }
    }
}

const validator = new Validation.LettersOnlyValidator()

八、装饰器

装饰器目前处于 Stage 3 提案,TypeScript 5.0+ 已支持新版装饰器规范。

// tsconfig.json 中需要开启(新版不需要额外配置)
// "experimentalDecorators": true  // 旧版装饰器
// TypeScript 5.0+ 默认支持新版装饰器

// 类装饰器
function sealed(constructor: Function) {
    Object.seal(constructor)
    Object.seal(constructor.prototype)
}

@sealed
class BugReport {
    type = 'report'
    title: string
    constructor(t: string) { this.title = t }
}

// 方法装饰器
function log(target: any, key: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value
    descriptor.value = function (...args: any[]) {
        console.log(`Calling ${key} with`, args)
        const result = original.apply(this, args)
        console.log(`Result:`, result)
        return result
    }
    return descriptor
}

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

// 属性装饰器
function required(target: any, key: string) {
    let value = target[key]
    Object.defineProperty(target, key, {
        get: () => value,
        set: (newValue) => {
            if (newValue === null || newValue === undefined) {
                throw new Error(`${key} is required`)
            }
            value = newValue
        }
    })
}

九、类型声明文件

.d.ts 文件结构

// types/global.d.ts —— 全局类型扩展

// 扩展已有模块(模块增强)
declare module 'vue' {
    interface ComponentCustomProperties {
        $myPlugin: MyPlugin
        $formatDate: (date: Date) => string
    }
}

// 扩展全局对象
declare global {
    interface Window {
        analytics: Analytics
        gtag: (...args: any[]) => void
    }

    interface ImportMeta {
        env: {
            VITE_API_URL: string
            VITE_APP_TITLE: string
            MODE: string
            DEV: boolean
            PROD: boolean
        }
    }
}

export {}   // 使文件成为模块(否则 declare global 不起作用)
// 为 JS 文件编写类型声明
// mylib.d.ts

export declare function init(options: InitOptions): void
export declare function destroy(): void

export declare interface InitOptions {
    container: string | HTMLElement
    theme?: 'light' | 'dark'
    lang?: string
}

export declare class EventEmitter {
    on(event: string, listener: Function): this
    off(event: string, listener: Function): this
    emit(event: string, ...args: any[]): boolean
}

环境声明

// 声明未经过模块化的全局变量(如通过 CDN 加载的库)
declare const jQuery: JQueryStatic
declare function alert(message: string): void

// 声明模块(当某个包没有类型声明时的临时方案)
declare module 'untyped-package' {
    export function doSomething(input: string): void
    export default class Main { ... }
}

// 声明文件类型(用于 Vite 项目导入资源)
declare module '*.svg' {
    const content: string
    export default content
}

declare module '*.png' {
    const content: string
    export default content
}

declare module '*.vue' {
    import type { DefineComponent } from 'vue'
    const component: DefineComponent<{}, {}, any>
    export default component
}

十、tsconfig.json 完整配置

{
  "compilerOptions": {
    // ============ 语言与环境 ============
    "target": "ES2022",
    // 编译输出的 JS 版本
    // ES3 / ES5 / ES6/ES2015 / ES2016~ES2022 / ESNext

    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    // 包含的内置类型声明库
    // DOM: 浏览器 API(window/document 等)
    // ES2022: 最新 ES 特性类型

    "useDefineForClassFields": true,
    // 使用 Object.defineProperty 定义类字段(与现代浏览器一致)
    // Vue 3 + Vite 项目必须为 true

    // ============ 模块系统 ============
    "module": "ESNext",
    // 模块格式:CommonJS / ES6 / ESNext / NodeNext / Preserve

    "moduleResolution": "bundler",
    // 模块解析策略
    // node: 传统 Node.js CJS 解析
    // bundler: 现代打包工具(Vite/webpack),可省略扩展名
    // NodeNext: Node.js 原生 ESM,要求完整扩展名

    "baseUrl": ".",
    // 路径别名的基准目录

    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"]
    },
    // 路径别名(Vite 中还需在 vite.config.ts 同步配置 resolve.alias)

    "resolveJsonModule": true,
    // 允许导入 .json 文件

    "allowImportingTsExtensions": true,
    // 允许 import './foo.ts'(需要 noEmit 或 emitDeclarationOnly)

    // ============ 输出控制 ============
    "noEmit": true,
    // 不输出编译文件(Vite 项目中 TS 只做类型检查,由 Vite 负责编译)

    "outDir": "./dist",
    // 编译输出目录(noEmit 为 true 时无效)

    "rootDir": "./src",
    // 源码根目录

    "declaration": true,
    // 生成 .d.ts 类型声明文件(发布库时需要)

    "declarationDir": "./dist/types",
    // 声明文件输出目录

    "sourceMap": true,
    // 生成 .js.map 调试映射文件

    "removeComments": false,
    // 编译时是否移除注释

    // ============ 类型检查严格模式 ============
    "strict": true,
    // 开启所有严格检查(等价于以下全部开启)

    "strictNullChecks": true,
    // null/undefined 不能赋给其他类型(strict 包含)

    "noImplicitAny": true,
    // 禁止参数/变量隐式推断为 any(strict 包含)

    "strictFunctionTypes": true,
    // 函数参数逆变检查(strict 包含)

    "strictBindCallApply": true,
    // bind/call/apply 的参数类型检查(strict 包含)

    "strictPropertyInitialization": true,
    // 类属性必须在构造函数中初始化(strict 包含)

    "noImplicitThis": true,
    // 禁止 this 隐式为 any(strict 包含)

    "useUnknownInCatchVariables": true,
    // catch 变量类型为 unknown 而非 any(strict 包含)

    // ============ 额外检查 ============
    "noUnusedLocals": true,
    // 不允许未使用的局部变量

    "noUnusedParameters": true,
    // 不允许未使用的函数参数
    // 用 _param 前缀表示故意不使用

    "noImplicitReturns": true,
    // 函数所有代码路径必须有返回值

    "noFallthroughCasesInSwitch": true,
    // switch case 必须有 break 或 return

    "noUncheckedIndexedAccess": true,
    // 索引访问类型包含 undefined(arr[0] 类型为 T | undefined)

    "exactOptionalPropertyTypes": true,
    // 可选属性不能赋值为 undefined(必须省略该键)

    // ============ 互操作性 ============
    "esModuleInterop": true,
    // 允许 import CJS 模块(如 import fs from 'fs')
    // 生成兼容 __esModule 标记的辅助代码

    "allowSyntheticDefaultImports": true,
    // 允许无默认导出的模块使用默认导入语法(esModuleInterop 自动开启)

    "forceConsistentCasingInFileNames": true,
    // 文件名大小写敏感(防止 macOS 不区分大小写导致的跨平台 bug)

    // ============ 跳过检查 ============
    "skipLibCheck": true
    // 跳过 .d.ts 文件的类型检查(加快编译,避免第三方库类型冲突)
  },

  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "vite.config.ts",
    "vitest.config.ts"
  ],

  "exclude": [
    "node_modules",
    "dist",
    "**/*.spec.ts",
    "**/*.test.ts"
  ]
}

多环境 tsconfig 分离

tsconfig.json              # 基础配置(被其他继承)
tsconfig.app.json          # 应用代码(src/)
tsconfig.node.json         # Node.js 配置(vite.config.ts 等)
tsconfig.vitest.json       # 测试配置
// tsconfig.json(基础,不直接使用)
{
  "compilerOptions": {
    "target": "ES2022",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

// tsconfig.app.json(应用代码)
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "noEmit": true,
    "useDefineForClassFields": true,
    "allowImportingTsExtensions": true,
    "paths": { "@/*": ["./src/*"] }
  },
  "include": ["src/**/*", "env.d.ts"]
}

// tsconfig.node.json(构建工具配置文件)
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022"],
    "noEmit": true
  },
  "include": ["vite.config.ts", "vitest.config.ts", "scripts/**/*"]
}

十一、Vue 3 + TypeScript 最佳实践

组件 Props 类型定义

<!-- UserCard.vue -->
<script setup lang="ts">
// 方式一:运行时声明(简单场景)
const props = defineProps({
  name: { type: String, required: true },
  age: { type: Number, default: 0 },
})

// 方式二:类型声明(推荐,更强的类型推断)
interface Props {
  name: string
  age?: number
  role: 'admin' | 'user' | 'guest'
  tags: string[]
  user: User
  onUpdate?: (user: User) => void
}

const props = defineProps<Props>()

// 方式三:类型声明 + 默认值(TypeScript 5.0+ / Vue 3.3+)
const props = withDefaults(defineProps<Props>(), {
  age: 0,
  role: 'user',
  tags: () => [],      // 数组/对象默认值必须用工厂函数
})
</script>

Emits 类型定义

<script setup lang="ts">
// 方式一:运行时声明
const emit = defineEmits(['update', 'delete'])

// 方式二:类型声明(推荐)
const emit = defineEmits<{
  update: [user: User]              // Vue 3.3+ 命名元组语法
  delete: [id: number]
  'status-change': [status: Status]
}>()

// 旧语法(3.3 之前)
const emit = defineEmits<{
  (e: 'update', user: User): void
  (e: 'delete', id: number): void
}>()

// 使用
emit('update', updatedUser)
emit('delete', user.id)
</script>

ref 与 reactive 类型

<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import type { Ref, ComputedRef } from 'vue'

// ref:TypeScript 自动推断或手动指定
const count = ref(0)                        // Ref<number>
const name = ref<string>('')               // Ref<string>
const user = ref<User | null>(null)         // Ref<User | null>

// 手动声明类型(不推荐,冗余)
const count2: Ref<number> = ref(0)

// reactive:推断对象类型
const state = reactive({
  users: [] as User[],
  loading: false,
  currentPage: 1,
})

// 如需复杂接口
interface AppState {
  users: User[]
  loading: boolean
  currentPage: number
}
const state2 = reactive<AppState>({
  users: [],
  loading: false,
  currentPage: 1,
})

// computed:自动推断返回类型
const fullName = computed(() => `${state2.users[0]?.name ?? ''}`)
// 类型:ComputedRef<string>

// watch:参数类型
watch(
  () => state2.currentPage,
  (newPage: number, oldPage: number) => {
    fetchUsers(newPage)
  },
  { immediate: true, deep: false }
)
</script>

组合式函数(Composables)类型

// composables/useUser.ts
import { ref, computed } from 'vue'
import type { Ref, ComputedRef } from 'vue'

interface User {
  id: number
  name: string
  email: string
}

interface UseUserReturn {
  user: Ref<User | null>
  loading: Ref<boolean>
  error: Ref<string | null>
  isLoggedIn: ComputedRef<boolean>
  fetchUser: (id: number) => Promise<void>
  logout: () => void
}

export function useUser(): UseUserReturn {
  const user = ref<User | null>(null)
  const loading = ref(false)
  const error = ref<string | null>(null)

  const isLoggedIn = computed(() => user.value !== null)

  async function fetchUser(id: number): Promise<void> {
    loading.value = true
    error.value = null
    try {
      const res = await fetch(`/api/users/${id}`)
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      user.value = await res.json() as User
    } catch (e) {
      error.value = e instanceof Error ? e.message : 'Unknown error'
    } finally {
      loading.value = false
    }
  }

  function logout(): void {
    user.value = null
  }

  return { user, loading, error, isLoggedIn, fetchUser, logout }
}

模板 ref 类型

<script setup lang="ts">
import { ref, onMounted } from 'vue'

// DOM 元素 ref
const inputRef = ref<HTMLInputElement | null>(null)
const canvasRef = ref<HTMLCanvasElement | null>(null)

// 子组件 ref
import MyModal from './MyModal.vue'
const modalRef = ref<InstanceType<typeof MyModal> | null>(null)

onMounted(() => {
  inputRef.value?.focus()              // 可选链处理 null
  modalRef.value?.open()
})
</script>

<template>
  <input ref="inputRef" type="text" />
  <MyModal ref="modalRef" />
</template>

Pinia Store 类型

// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

interface User {
  id: number
  name: string
  role: 'admin' | 'user'
}

// Setup Store(推荐,类型推断最好)
export const useUserStore = defineStore('user', () => {
  const currentUser = ref<User | null>(null)
  const token = ref<string>('')

  const isAdmin = computed(() => currentUser.value?.role === 'admin')
  const isLoggedIn = computed(() => !!token.value)

  async function login(credentials: { email: string; password: string }): Promise<void> {
    const res = await fetch('/api/auth/login', {
      method: 'POST',
      body: JSON.stringify(credentials),
    })
    const data = await res.json() as { user: User; token: string }
    currentUser.value = data.user
    token.value = data.token
  }

  function logout(): void {
    currentUser.value = null
    token.value = ''
  }

  return { currentUser, token, isAdmin, isLoggedIn, login, logout }
})

// 在组件中使用
import { storeToRefs } from 'pinia'
const store = useUserStore()
const { currentUser, isAdmin } = storeToRefs(store)  // 保持响应式
const { login, logout } = store                       // 方法直接解构

Vue Router 类型

// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'home',
    component: () => import('@/views/HomeView.vue'),
  },
  {
    path: '/user/:id',
    name: 'user-detail',
    component: () => import('@/views/UserDetail.vue'),
    meta: { requiresAuth: true },
  },
]

// 扩展路由 meta 类型
declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
    title?: string
    roles?: string[]
  }
}

// 在组件中使用
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()

const userId = route.params.id as string
const query = route.query.search as string | undefined

router.push({ name: 'user-detail', params: { id: '123' } })

API 请求类型封装

// utils/request.ts
interface RequestConfig {
  url: string
  method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
  params?: Record<string, string | number | boolean>
  data?: unknown
  headers?: Record<string, string>
}

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

async function request<T>(config: RequestConfig): Promise<T> {
  const { url, method = 'GET', params, data, headers } = config

  const searchParams = params
    ? '?' + new URLSearchParams(
        Object.fromEntries(
          Object.entries(params).map(([k, v]) => [k, String(v)])
        )
      ).toString()
    : ''

  const res = await fetch(`/api${url}${searchParams}`, {
    method,
    headers: { 'Content-Type': 'application/json', ...headers },
    body: data ? JSON.stringify(data) : undefined,
  })

  if (!res.ok) throw new Error(`HTTP Error: ${res.status}`)

  const json = await res.json() as ApiResponse<T>
  if (json.code !== 0) throw new Error(json.message)

  return json.data
}

// API 模块
// api/user.ts
export interface User {
  id: number
  name: string
  email: string
}

export interface CreateUserDTO {
  name: string
  email: string
  password: string
}

export const userApi = {
  getList: (page = 1, size = 20) =>
    request<{ list: User[]; total: number }>({
      url: '/users',
      params: { page, size },
    }),

  getById: (id: number) =>
    request<User>({ url: `/users/${id}` }),

  create: (dto: CreateUserDTO) =>
    request<User>({ url: '/users', method: 'POST', data: dto }),

  update: (id: number, dto: Partial<CreateUserDTO>) =>
    request<User>({ url: `/users/${id}`, method: 'PUT', data: dto }),

  remove: (id: number) =>
    request<void>({ url: `/users/${id}`, method: 'DELETE' }),
}

十二、项目文件结构

Vue 3 + TypeScript + Vite 推荐结构

my-project/
├── public/                         # 静态资源(不经过 Vite 处理)
│   └── favicon.ico
│
├── src/
│   ├── main.ts                     # 应用入口
│   ├── App.vue                     # 根组件
│   ├── env.d.ts                    # 环境变量和模块类型声明
│   │
│   ├── assets/                     # 静态资源(经过 Vite 处理)
│   │   ├── images/
│   │   └── styles/
│   │       ├── main.css
│   │       └── variables.css
│   │
│   ├── components/                 # 公共组件
│   │   ├── base/                   # 基础 UI 组件(Button、Input 等)
│   │   │   ├── BaseButton.vue
│   │   │   └── BaseInput.vue
│   │   ├── layout/                 # 布局组件
│   │   │   ├── AppHeader.vue
│   │   │   ├── AppSidebar.vue
│   │   │   └── AppFooter.vue
│   │   └── common/                 # 业务通用组件
│   │       ├── UserAvatar.vue
│   │       └── DataTable.vue
│   │
│   ├── views/                      # 页面级组件(路由直接映射)
│   │   ├── HomeView.vue
│   │   ├── user/
│   │   │   ├── UserListView.vue
│   │   │   └── UserDetailView.vue
│   │   └── auth/
│   │       ├── LoginView.vue
│   │       └── RegisterView.vue
│   │
│   ├── router/                     # 路由
│   │   ├── index.ts                # 路由实例创建
│   │   ├── guards.ts               # 路由守卫
│   │   └── routes/
│   │       ├── index.ts            # 汇总所有路由
│   │       ├── user.routes.ts
│   │       └── auth.routes.ts
│   │
│   ├── stores/                     # Pinia 状态管理
│   │   ├── index.ts                # 统一导出
│   │   ├── user.store.ts
│   │   └── app.store.ts
│   │
│   ├── composables/                # 组合式函数
│   │   ├── useUser.ts
│   │   ├── usePagination.ts
│   │   ├── useForm.ts
│   │   └── useRequest.ts
│   │
│   ├── api/                        # API 请求层
│   │   ├── request.ts              # axios/fetch 封装
│   │   ├── user.api.ts
│   │   └── auth.api.ts
│   │
│   ├── types/                      # 全局类型定义
│   │   ├── index.ts                # 统一导出
│   │   ├── user.types.ts
│   │   ├── api.types.ts            # 请求/响应通用类型
│   │   └── global.d.ts             # 全局声明扩展
│   │
│   ├── utils/                      # 工具函数
│   │   ├── index.ts
│   │   ├── date.ts
│   │   ├── format.ts
│   │   └── validate.ts
│   │
│   └── constants/                  # 常量
│       ├── index.ts
│       └── api.constants.ts
│
├── tests/                          # 测试
│   ├── unit/
│   └── e2e/
│
├── .eslintrc.cjs                   # ESLint 配置
├── .prettierrc                     # Prettier 配置
├── vite.config.ts                  # Vite 配置
├── vitest.config.ts                # 测试配置
├── tsconfig.json                   # TS 基础配置
├── tsconfig.app.json               # 应用代码 TS 配置
├── tsconfig.node.json              # 构建工具 TS 配置
└── package.json

vite.config.ts 参考配置

import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],

  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
      '@components': fileURLToPath(new URL('./src/components', import.meta.url)),
      '@utils': fileURLToPath(new URL('./src/utils', import.meta.url)),
    },
  },

  server: {
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },

  build: {
    target: 'es2022',
    outDir: 'dist',
  },
})

env.d.ts(环境变量类型)

/// <reference types="vite/client" />

// 扩展 ImportMeta.env 的类型
interface ImportMetaEnv {
  readonly VITE_API_BASE_URL: string
  readonly VITE_APP_TITLE: string
  readonly VITE_APP_VERSION: string
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

// 声明静态资源模块
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

declare module '*.svg' {
  const content: string
  export default content
}

declare module '*.png' {
  const content: string
  export default content
}

十三、最佳实践

1. 严格模式全开,从不关闭

// tsconfig.json
{
  "compilerOptions": {
    "strict": true   // 无论如何都开启
  }
}

2. 优先使用 unknown 而非 any

// 不好:any 关闭了所有类型检查
function parse(data: any) {
  return data.name   // 无类型检查,运行时可能崩溃
}

// 好:unknown 使用前必须收窄
function parse(data: unknown): string {
  if (typeof data === 'object' && data !== null && 'name' in data) {
    return String((data as { name: unknown }).name)
  }
  throw new Error('Invalid data')
}

3. 类型收窄而非断言

// 不好:强制断言,绕过类型系统
const name = (user as any).name

// 好:类型守卫,TypeScript 帮你验证
function getName(user: unknown): string {
  if (isUser(user)) return user.name
  throw new Error('Not a user')
}

4. 使用 const 断言固化字面量

// 不好:类型为 string,失去精确信息
const ROLES = ['admin', 'user', 'guest']   // string[]

// 好:类型为 readonly ["admin", "user", "guest"]
const ROLES = ['admin', 'user', 'guest'] as const

type Role = typeof ROLES[number]   // 'admin' | 'user' | 'guest'

// 不好:config.method 类型为 string
const config = { method: 'GET', url: '/api' }

// 好:config.method 类型为 "GET"
const config = { method: 'GET', url: '/api' } as const

5. 利用判别联合类型处理状态

// 不好:多个可选字段,状态不清晰
interface RequestState {
  loading: boolean
  data?: User
  error?: string
}

// 好:判别联合类型,状态互斥且明确
type RequestState =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: User }
  | { status: 'error'; error: string }

// 使用时 TypeScript 帮你保证每种状态下的字段
function render(state: RequestState) {
  switch (state.status) {
    case 'loading':
      return '<Spinner />'
    case 'success':
      return state.data.name   // 类型安全,data 一定存在
    case 'error':
      return state.error       // 类型安全,error 一定存在
    default:
      return null
  }
}

6. 使用 import type 减少运行时依赖

// 纯类型导入,编译后完全消除
import type { User, ApiResponse } from './types'
import type { Ref, ComputedRef } from 'vue'

// 混合导入(TypeScript 4.5+)
import { ref, type Ref } from 'vue'

7. 为异步函数显式声明返回类型

// 不好:返回类型依赖推断,内部实现变化可能改变外部 API
async function getUser(id: number) {
  const res = await fetch(`/api/users/${id}`)
  return res.json()
}

// 好:明确契约,内部实现不影响类型签名
async function getUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`)
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
  return res.json() as Promise<User>
}

8. 避免枚举,优先使用 const + 字面量类型

// 不推荐:enum 在运行时生成额外代码
enum Direction {
  Up = 'UP',
  Down = 'DOWN',
}

// 推荐:const 对象 + 类型提取(零运行时开销)
const Direction = {
  Up: 'UP',
  Down: 'DOWN',
} as const

type Direction = typeof Direction[keyof typeof Direction]
// 'UP' | 'DOWN'

9. 工具函数使用泛型保持类型信息

// 不好:返回 any,类型信息丢失
function first(arr: any[]): any {
  return arr[0]
}

// 好:泛型保留类型
function first<T>(arr: T[]): T | undefined {
  return arr[0]
}

const name = first(['Alice', 'Bob'])   // 类型为 string | undefined
const num = first([1, 2, 3])           // 类型为 number | undefined

10. 使用 satisfies 验证类型而不改变推断

// as 会改变推断类型(可能过于宽泛)
const config = {
  colors: { red: '#ff0000', green: '#00ff00' }
} as Record<string, { [key: string]: string }>

config.colors.red   // 类型为 string(正确)
config.colors.red.length  // 类型为 string(但 IDE 知道值)

// satisfies 验证类型但保留推断(TypeScript 4.9+)
const config2 = {
  colors: { red: '#ff0000', green: '#00ff00' }
} satisfies Record<string, Record<string, string>>

config2.colors.red         // 类型仍为 string
config2.colors.unknownKey  // Error:没有这个键

十四、常见陷阱与注意事项

1. strictPropertyInitialization:类属性必须初始化

// 错误:启用 strict 后,未初始化会报错
class User {
  name: string   // Error: Property 'name' has no initializer
}

// 解决方案一:构造函数初始化
class User {
  name: string
  constructor(name: string) { this.name = name }
}

// 解决方案二:给默认值
class User {
  name: string = ''
}

// 解决方案三:可选属性
class User {
  name?: string
}

// 解决方案四:非空断言(确定会在构造后赋值时)
class User {
  name!: string   // ! 告诉 TS "我保证会赋值"
}

2. 类型断言不是类型转换

// as 只是告诉 TypeScript 类型,不做运行时转换
const num = '42' as unknown as number
console.log(typeof num)   // 'string'(还是字符串!)
console.log(num + 1)      // '421'(字符串拼接)

// 真正的转换
const realNum = Number('42')

3. 对象字面量的多余属性检查

interface User { name: string; age: number }

// 直接赋给有类型的变量:多余属性会报错
const u: User = { name: 'Alice', age: 25, email: '[email protected]' }
// Error: Object literal may only specify known properties

// 中间变量赋值:不检查(新鲜度检查只在直接赋值时触发)
const obj = { name: 'Alice', age: 25, email: '[email protected]' }
const u2: User = obj   // 正确(没有新鲜度检查)

4. 联合类型中 never 的妙用:穷举检查

type Shape = 'circle' | 'square' | 'triangle'

function getArea(shape: Shape): number {
  switch (shape) {
    case 'circle': return 100
    case 'square': return 200
    // 忘记处理 triangle
    default:
      // 利用 never 做穷举检查:如果有未处理的分支,这里会报错
      const exhaustive: never = shape
      // Error: Type 'string' is not assignable to type 'never'
      throw new Error(`Unhandled shape: ${exhaustive}`)
  }
}

5. 可选链与非空断言的区别

const user: User | null = getUser()

// 可选链:user 为 null/undefined 时返回 undefined
const name = user?.name              // string | undefined

// 非空断言:告诉 TS 绝对不为 null(如果是 null 则运行时报错)
const name2 = user!.name            // string(如果 user 为 null 则崩溃)

// 最安全:先检查
if (user) {
  const name3 = user.name           // string(TypeScript 确保)
}

6. noUncheckedIndexedAccess 下的数组访问

// 开启 noUncheckedIndexedAccess 后
const arr: number[] = [1, 2, 3]
const first = arr[0]   // 类型为 number | undefined(不再是 number)

// 必须处理可能的 undefined
if (first !== undefined) {
  console.log(first.toFixed(2))
}

// 或用可选链
console.log(first?.toFixed(2))

7. 泛型的默认推断行为

function createArray<T>(): T[] { return [] }

// 没有类型推断来源时必须显式指定
const arr1 = createArray()           // T 推断为 unknown
const arr2 = createArray<string>()   // 显式指定,正确

// 有推断来源时可以省略
function wrapInArray<T>(value: T): T[] { return [value] }
const arr3 = wrapInArray('hello')    // 推断为 string[]

8. Vue 中的 defineProps 解构丢失响应式

// 错误:解构后 name 和 age 不再是响应式的
const { name, age } = defineProps<{ name: string; age: number }>()

// 正确方式一:通过 props 访问
const props = defineProps<{ name: string; age: number }>()
watch(() => props.name, ...)

// 正确方式二:Vue 3.5+ 支持响应式 props 解构
// 需要在 vite.config.ts 启用 propsDestructure 特性
const { name } = defineProps<{ name: string }>()
// 3.5+ 中这是响应式的

9. 避免使用 Function 类型

// 不好:Function 类型太宽泛,丢失参数和返回类型信息
function call(fn: Function) { fn() }

// 好:明确函数签名
function call(fn: () => void) { fn() }
function callWith<T>(fn: (arg: T) => void, arg: T) { fn(arg) }

// 接受任意函数(用于高阶函数工具)
function memoize<T extends (...args: any[]) => any>(fn: T): T { ... }

10. 类型导出的一致性

// 不好:类型和值混在一起导入
import { User, createUser, UserRole } from './user'
// 不清楚哪些是类型,哪些是值

// 好:类型单独用 import type
import type { User, UserRole } from './user'
import { createUser } from './user'

// 或混合语法(TypeScript 4.5+)
import { createUser, type User, type UserRole } from './user'

最佳实践

优先用 interface 声明对象类型,用 type 声明联合类型和工具类型interface 支持声明合并(多处 interface Foo 会自动合并),适合描述对象形状和可扩展的 API;type 更适合联合类型、交叉类型、映射类型等高级类型操作。

// 推荐:对象形状用 interface
interface User { id: number; name: string }

// 推荐:联合类型用 type
type Status = 'pending' | 'active' | 'inactive'
type UserOrAdmin = User | Admin

satisfies 运算符在保留字面量类型的同时进行类型校验as 断言会放弃类型检查;satisfies 既校验类型约束,又保留推断出的更精确类型(字面量类型)。

const config = {
  port: 3000,
  host: "localhost",
} satisfies Record<string, string | number>
// config.port 类型是 number(而非 string | number)

善用内置工具类型Partial<T>Required<T>Pick<T, K>Omit<T, K>Readonly<T>Record<K, V> 等工具类型可以组合出大多数常见类型变换,避免手写重复类型。

type CreateUserDto = Omit<User, 'id' | 'createdAt'>
type UpdateUserDto = Partial<CreateUserDto>

设置 tsconfig 严格模式"strict": true 包含了 strictNullChecksnoImplicitAny 等多项检查,新项目从一开始就开启,旧项目逐步迁移。

import type 明确导入类型,避免运行时副作用:类型在编译后被擦除,用 import type 让打包工具知道这是仅类型导入,可以安全地做 tree-shaking。


常见陷阱

陷阱:as 类型断言绕过了类型检查

现象: 使用 as 强制断言后,代码运行时报属性不存在或类型错误,但编译期没有警告。

原因: as 告诉 TypeScript "相信我,它就是这个类型",编译器放弃进一步检查。错误断言使类型安全失效。

解决: 用类型守卫(typeofinstanceof、用户定义类型谓词)替代 as;必须用 as 时先用 unknown 中转,减少意外断言风险。

// 危险:直接断言,类型系统失效
const user = response.data as User

// 安全:先验证再使用
function isUser(obj: unknown): obj is User {
  return typeof obj === 'object' && obj !== null && 'id' in obj
}
if (isUser(response.data)) {
  console.log(response.data.id)  // 类型安全
}

陷阱:any 类型感染使类型检查形同虚设

现象: 文件有很多类型注解,但 TypeScript 没有发现明显的类型错误,等到运行时才报错。

原因: 某个变量被标注或推断为 any,它的所有后续操作(访问属性、传参等)都失去类型检查,错误被隐藏。

解决: 开启 "noImplicitAny": true;用 unknown 替代 any(强制在使用前做类型收窄)。

// any:完全跳过检查
function parse(raw: any) { return raw.name }  // 无错误但不安全

// unknown:强制类型收窄
function parse(raw: unknown) {
  if (typeof raw === 'object' && raw !== null && 'name' in raw) {
    return (raw as { name: string }).name
  }
}

陷阱:泛型约束不当导致 "Type X is not assignable to type Y"

现象: 自定义泛型函数调用时报类型不兼容错误,但看起来类型是对的。

原因: 泛型约束(extends)定义不够精确,或函数返回类型与泛型参数不兼容。

解决: 明确泛型约束,用 keyof T 约束对象键,用条件类型精确推导返回值类型。

// 错误:约束不足
function getField<T>(obj: T, key: string): any { ... }

// 正确:用 keyof 约束键
function getField<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}
const name = getField({ name: 'Alice', age: 25 }, 'name')  // string

参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog