TypeScript 常见踩坑
大白话解释: TypeScript 就像"严格的编辑器"。它会在你写代码时就告诉你哪里可能出错,而不是等到运行时才发现。但有时候它太严格了,会让你觉得"为什么要这样写?"
为什么要用 TypeScript?
- 提前发现错误:写代码时就能看到红色波浪线,不用等到运行时才发现
- 智能提示:编辑器能知道变量是什么类型,自动补全属性和方法
- 代码更易读:类型就是最好的文档,看类型就知道函数怎么用
常见踩坑原因:
- TypeScript 的类型系统是"结构化类型"(Structural Typing,也叫鸭子类型):只要两个类型的结构相同(属性和方法匹配),就认为它们兼容,不关心类型名称。这和 Java/C# 的"名义类型"(必须显式声明实现)不同
- 有些写法是为了绕过 TypeScript 的检查,但可能带来运行时风险
TypeScript 开发中高频遇到的类型、配置和运行时问题。
预防措施
- 严格模式配置:tsconfig.json 中开启
strict: true,启用所有严格检查 - 避免使用 any:优先使用
unknown+ 类型收窄,而非any绕过检查 - 类型定义文件管理:项目自定义类型放在
src/types/目录,第三方库缺失类型时补充.d.ts - 渐进式引入:大型 JS 项目可先用
allowJs: true逐步迁移 - 利用类型推导:善用
typeof、keyof、infer等关键字,减少手动类型声明
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 User2. 非空断言的隐患
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对象替代枚举,编译后只保留值