Skip to content

TypeScript 常见踩坑

大白话解释: TypeScript 就像"严格的编辑器"。它会在你写代码时就告诉你哪里可能出错,而不是等到运行时才发现。但有时候它太严格了,会让你觉得"为什么要这样写?"

为什么要用 TypeScript?

  • 提前发现错误:写代码时就能看到红色波浪线,不用等到运行时才发现
  • 智能提示:编辑器能知道变量是什么类型,自动补全属性和方法
  • 代码更易读:类型就是最好的文档,看类型就知道函数怎么用

常见踩坑原因:

  • TypeScript 的类型系统是"结构化类型"(Structural Typing,也叫鸭子类型):只要两个类型的结构相同(属性和方法匹配),就认为它们兼容,不关心类型名称。这和 Java/C# 的"名义类型"(必须显式声明实现)不同
  • 有些写法是为了绕过 TypeScript 的检查,但可能带来运行时风险

TypeScript 开发中高频遇到的类型、配置和运行时问题。


预防措施

  1. 严格模式配置:tsconfig.json 中开启 strict: true,启用所有严格检查
  2. 避免使用 any:优先使用 unknown + 类型收窄,而非 any 绕过检查
  3. 类型定义文件管理:项目自定义类型放在 src/types/ 目录,第三方库缺失类型时补充 .d.ts
  4. 渐进式引入:大型 JS 项目可先用 allowJs: true 逐步迁移
  5. 利用类型推导:善用 typeofkeyofinfer 等关键字,减少手动类型声明

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

ts
interface User {
  name: string              // 姓名
  age: number               // 年龄
}

// ❌ 直接传入字面量会报错(多余属性检查)
// TypeScript 对字面量有更严格的检查:不允许出现接口中未定义的属性
const user: User = { name: '张三', age: 25, gender: '男' }
// Error: 对象字面量只能指定已知属性

// ✅ 通过变量赋值绕过(结构化类型兼容)
// 先赋值给变量,再将变量赋给 User 类型,TS 只检查结构是否匹配
const data = { name: '张三', age: 25, gender: '男' }
const user: User = data   // 不报错,因为 data 包含 User 所需的所有属性

// ✅ 或用类型断言(告诉 TS "我知道我在做什么")
const user = { name: '张三', age: 25, gender: '男' } as User

2. 非空断言的隐患

ts
const el = document.getElementById('app')

// ❌ 非空断言:你告诉 TS 这个值一定不是 null
el!.style.color = 'red'   // 编译通过,但运行时可能为 null

// ✅ 先判断再使用
if (el) {
  el.style.color = 'red'
}

// ✅ 或用可选链(安全但 el 可能为 null)
el?.style.color = 'red'

3. 类型收窄(Type Narrowing)

ts
function example(value: string | number) {
  // ❌ 直接调用字符串方法报错
  value.toUpperCase()  // Error: number 上没有 toUpperCase

  // ✅ typeof 收窄
  if (typeof value === 'string') {
    value.toUpperCase()  // 这里 value 被收窄为 string
  }

  // ✅ in 操作符收窄
  if ('length' in value) {
    console.log(value.length)  // 收窄为有 length 属性的类型
  }
}

4. 枚举的坑

什么是枚举(Enum)? 枚举是一种特殊的类型,用于定义一组命名常量。TypeScript 中有数字枚举和字符串枚举两种。 数字枚举存在"反向映射"(可以通过值获取名称),字符串枚举没有。

ts
// 数字枚举存在反向映射(值 → 名称)
enum Status {
  Success = 0,              // 成功状态码
  Fail = 1                  // 失败状态码
}
console.log(Status[0])     // 'Success'(反向映射:通过值获取名称)
console.log(Status.Fail)   // 1(正向映射:通过名称获取值)

// ❌ 字符串枚举没有反向映射
enum Color {
  Red = 'red',
  Blue = 'blue'
}
console.log(Color['red'])  // undefined(字符串枚举不支持反向映射)

// ✅ 推荐用 as const 对象替代枚举(更轻量,编译后代码更少)
const Status = {
  Success: 0,
  Fail: 1
} as const
type StatusType = typeof Status[keyof typeof Status]  // 类型为 0 | 1(类型名不能与变量名重名)

5. 泛型约束

什么是泛型(Generics)? 泛型是一种参数化类型的机制,允许在定义函数、接口或类时不指定具体类型,而在使用时再指定。 泛型用 <T> 表示,T 是类型参数,调用时会被替换为实际类型。

ts
// ❌ 泛型默认是 unknown,不能访问属性
function getLength<T>(value: T) {
  return value.length  // Error: unknown 上没有 length
}

// ✅ 用 extends 约束泛型(T 必须有 length 属性)
function getLength<T extends { length: number }>(value: T) {
  return value.length  // OK,T 被约束为有 length 属性的类型
}

getLength('hello')     // OK,string 有 length
getLength([1, 2, 3])   // OK,array 有 length
getLength(123)         // Error: number 没有 length 属性

6. 接口 vs 类型别名

ts
// 接口可以扩展(extends),类型别名用交叉(&)
interface Animal { name: string }
interface Dog extends Animal { bark(): void }

type Animal2 = { name: string }
type Dog2 = Animal2 & { bark(): void }

// 接口可以声明合并(同名自动合并)
interface Window { myProp: string }  // 扩展内置类型

// 声明合并的常见用途:
// 1. 扩展 Window 接口(挂载全局变量)
declare global {
  interface Window {
    __APP_CONFIG__: { apiUrl: string }
  }
}
window.__APP_CONFIG__.apiUrl  // ✅ 类型安全

// 2. 扩展第三方库类型(补充未导出的类型)
declare module 'some-lib' {
  interface Options {
    customProp?: string  // 补充官方未声明的选项
  }
}
ts
// 用 type 的场景
// 联合类型
type Status = 'success' | 'error' | 'loading'

// 元组
type Point = [number, number]

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

7. any vs unknown

ts
// any:关闭类型检查,可以做任何操作
let a: any = 'hello'
a.foo()      // 不报错(运行时出错)

// unknown:安全的 any,必须先检查类型
let b: unknown = 'hello'
b.foo()      // 报错:unknown 上不能调用方法

if (typeof b === 'string') {
  b.toUpperCase()  // OK
}

8. 模块导入报错

ts
// ❌ 导入没有类型的第三方库报错
import { someFunc } from 'my-lib'
// Error: 找不到模块 'my-lib' 的类型声明

// ✅ 方式一:声明模块
// src/types/my-lib.d.ts
declare module 'my-lib' {
  export function someFunc(): void
}

// ✅ 方式二:安装类型包
// npm install -D @types/lodash

// ✅ 方式三:允许导入任意模块
// tsconfig.json
{
  "compilerOptions": {
    "moduleResolution": "bundler",
    "allowJs": true
  }
}

9. Vue 组件类型

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

// ref 自动推导类型
const count = ref(0)        // Ref<number>
count.value = 'hello'       // ❌ Error

// 显式指定类型
const user = ref<{ name: string; age: number } | null>(null)

// computed 自动推导
const double = computed(() => count.value * 2)  // ComputedRef<number>

// defineProps 类型
const props = defineProps<{
  title: string
  count?: number
  list: string[]
}>()

// defineEmits 类型
const emit = defineEmits<{
  (e: 'update', value: string): void
  (e: 'close'): void
}>()

// 模板 ref 类型
const el = ref<HTMLInputElement | null>(null)
onMounted(() => {
  el.value?.focus()
})
</script>

10. 条件类型推断

ts
// infer 关键字提取类型
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never

type A = ReturnOf<() => string>       // string
type B = ReturnOf<() => number[]>     // number[]

// 提取 Promise 内部类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T
type C = UnwrapPromise<Promise<string>>  // string

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

真实场景

场景 1:调用 DOM API 时报错"对象可能为 null"

  • 问题document.getElementById('app') 返回值可能是 null,直接调用方法会报错
  • 原因:TypeScript 严格模式下,DOM 查询返回 HTMLElement | null
  • 解决:先判断 if (el) 再使用,或使用可选链 el?.style.color = 'red'

场景 2:第三方库没有类型声明

  • 问题:引入老版本的 JS 库,TypeScript 报错"找不到模块的类型声明"
  • 原因:该库没有自带类型定义,也没有 @types
  • 解决:在 src/types/ 下创建 .d.ts 文件,用 declare module 声明类型

场景 3:枚举在运行时体积膨胀

  • 问题:使用 TypeScript 枚举后,打包产物多了很多代码
  • 原因:数字枚举编译后会生成双向映射对象,体积翻倍
  • 解决:改用 as const 对象替代枚举,编译后只保留值

参考

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准