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模块系统
目录
- 基础类型系统
- 函数类型
- 接口与类型别名
- 类
- 泛型
- 高级类型
- 模块与命名空间
- 装饰器
- 类型声明文件
- tsconfig.json 完整配置
- Vue 3 + TypeScript 最佳实践
- 项目文件结构
- 最佳实践
- 常见陷阱与注意事项
一、基础类型系统
原始类型
// 基本类型注解
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 |
可被继承、可合并扩展 |
| 描述函数签名 | type 或 interface 均可 |
风格统一即可 |
| 联合类型、元组类型 | 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 包含了 strictNullChecks、noImplicitAny 等多项检查,新项目从一开始就开启,旧项目逐步迁移。
用 import type 明确导入类型,避免运行时副作用:类型在编译后被擦除,用 import type 让打包工具知道这是仅类型导入,可以安全地做 tree-shaking。
常见陷阱
陷阱:as 类型断言绕过了类型检查
现象: 使用 as 强制断言后,代码运行时报属性不存在或类型错误,但编译期没有警告。
原因: as 告诉 TypeScript "相信我,它就是这个类型",编译器放弃进一步检查。错误断言使类型安全失效。
解决: 用类型守卫(typeof、instanceof、用户定义类型谓词)替代 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