JavaScript / TypeScript 易踩坑知识点 —— TypeScript 专属类
一、滥用 any
问题说明
any会完全关闭 TypeScript 的类型检查,等于放弃了 TS 的核心价值。
踩坑示例
// ❌ 滥用 anyconstdata:any=fetchData()data.someMethod()// 编译不报错,运行时可能报错// ❌ 用 any 当万能类型functionhandleInput(val:any){val.length// 如果传进来不是数组或字符串,运行时报错}// ✅ 用 unknown 替代 any(安全类型)constdata:unknown=fetchData()// data.someMethod() // ❌ 编译报错,必须先类型收窄if(typeofdata==='object'&&data!==null&&'someMethod'indata){data.someMethod()// ✅ 类型收窄后可以调用}// ✅ 用类型断言(确定类型时用)constdata=fetchData()asApiResponse data.items// ✅ 类型安全any 与 unknown 的区别
| 特性 | any | unknown |
|---|---|---|
| 可以调用任意方法 | ✅ | ❌(需先收窄) |
| 可以赋给任意类型 | ✅ | ✅ |
| 任意类型可赋给它 | ✅ | ❌(需先收窄) |
| 安全性 | 不安全 | 安全 |
二、函数重载顺序
问题说明
TypeScript 函数重载时,更具体的签名要放在更通用的前面,否则会被"吞掉"。
踩坑示例
// ❌ 顺序错误:通用签名在前,具体签名永远到不了functionhandleValue(val:string|number):voidfunctionhandleValue(val:string):void{console.log('string:',val.toUpperCase())}functionhandleValue(val:number):void{console.log('number:',val.toFixed(2))}// 调用时handleValue(42)// ❌ 编译报错:没有匹配的重载(string | number 匹配了第一个)// ✅ 顺序正确:具体签名在前functionhandleValue(val:string):voidfunctionhandleValue(val:number):voidfunctionhandleValue(val:string|number):void{if(typeofval==='string'){console.log('string:',val.toUpperCase())}else{console.log('number:',val.toFixed(2))}}handleValue(42)// ✅ 正确三、strictNullChecks 未开启
问题说明
不开启strictNullChecks时,null和undefined可以赋给任何类型,很多 bug 会在运行时才暴露。
踩坑示例
// tsconfig.json 中必须开启{"compilerOptions":{"strictNullChecks":true}}// 开启后:letname:stringname=null// ❌ 编译报错name=undefined// ❌ 编译报错name='hello'// ✅// 允许 null/undefined 时,用联合类型letname:string|null=nullname='hello'// ✅name=null// ✅// 非空断言操作符 !(告诉编译器"一定不为空")letname:string|null=nullname='hello'console.log(name!.length)// ✅ 告诉编译器 name 不为 null// ⚠️ 但如果 name 真的是 null,运行时会报错非空断言操作符 ! 的坑
// ❌ 滥用非空断言,运行时可能崩溃constelement=document.getElementById('my-id')!element.textContent='hello'// 如果元素不存在,运行时 TypeError// ✅ 先做类型守卫constelement=document.getElementById('my-id')if(element){element.textContent='hello'// ✅ 类型安全}// ✅ 可选链 + 空值合并document.getElementById('my-id')?.textContent='hello'四、类型断言 as 的坑
问题说明
as只是告诉编译器"相信我,这个类型是对的",但不会做运行时检查。用错了会导致运行时错误。
踩坑示例
// ❌ 断言错误类型,运行时崩溃constdata=JSON.parse('{"name": "Alice"}')asstring[]data[0].toUpperCase()// ❌ TypeError: data[0].toUpperCase is not a function// ✅ 正确断言constdata=JSON.parse('{"name": "Alice"}')as{name:string}data.name.toUpperCase()// ✅// ✅ 更好的方式:定义接口 + 类型守卫interfaceUser{name:stringage:number}functionisUser(val:unknown):valisUser{returntypeofval==='object'&&val!==null&&'name'inval}constdata=JSON.parse('{"name": "Alice", "age": 25}')if(isUser(data)){console.log(data.name)// ✅ 类型安全}五、泛型约束
问题说明
泛型不加约束时,只能使用object上共有的方法,无法访问特定属性。
踩坑示例
// ❌ 没有约束,无法访问 length 属性functiongetLength<T>(val:T):number{returnval.length// ❌ 编译报错:T 上不存在 length}// ✅ 添加约束functiongetLength<Textends{length:number}>(val:T):number{returnval.length// ✅}// ✅ 更精确的约束functiongetProperty<T,KextendskeyofT>(obj:T,key:K):T[K]{returnobj[key]}constuser={name:'Alice',age:25}getProperty(user,'name')// ✅getProperty(user,'address')// ❌ 编译报错六、Record 与 Record<string, any>
问题说明
Record<K, V>是 TS 内置的工具类型,但Record<string, any>等同于{},失去了类型检查的意义。
踩坑示例
// ❌ 等同于 {},没有类型检查constconfig:Record<string,any>={port:3000,host:'localhost'}config.port.toFixed(2)// ✅ 但 port 可能是 string,运行时可能报错// ✅ 定义明确的类型interfaceServerConfig{port:numberhost:stringdebug?:boolean}constconfig:ServerConfig={port:3000,host:'localhost'}config.port.toFixed(2)// ✅ 类型安全config.timeout// ❌ 编译报错:timeout 不存在七、interface 与 type 的选择
问题说明
interface和type在很多场景下可以互换,但有一些关键区别。
对比表
| 特性 | interface | type |
|---|---|---|
| 对象类型定义 | ✅ | ✅ |
| 类型别名(联合/交叉) | ❌ | ✅ |
| 声明合并 | ✅ | ❌ |
| 类实现 | ✅ | ✅(type 用 intersection) |
| 计算属性名 | ❌ | ✅ |
踩坑示例
// interface 可以声明合并interfaceUser{name:string}interfaceUser{age:number}// User = { name: string, age: number }// type 不能合并,但可以做联合类型typeStatus='active'|'inactive'|'pending'// type 可以做交叉类型typeAdminUser=User&{role:'admin'}// 类实现 interfaceinterfaceIAnimal{name:stringspeak():void}classDogimplementsIAnimal{name='Dog'speak(){console.log('Woof')}}八、never 类型
问题说明
never表示"永远不会发生的值",用于 exhaustive check 和确定函数不会返回。
踩坑示例
// 穷举检查(exhaustive check)typeShape='circle'|'square'|'triangle'functiongetArea(shape:Shape):number{switch(shape){case'circle':returnMath.PI*1// 假设半径为1case'square':return1case'triangle':return0.5// ✅ 如果新增了 'pentagon',这里会报错default:{const_exhaustiveCheck:never=shapereturn_exhaustiveCheck// ❌ 如果 shape 类型扩展了,这里会报类型错误}}}// 标记函数永远不会返回functionthrowError(msg:string):never{thrownewError(msg)}functionassert(condition:boolean,msg:string):assertscondition{if(!condition){thrownewError(msg)}}总结对照表
| 坑点 | 错误写法 | 正确写法 |
|---|---|---|
| 滥用 any | const x: any = ... | const x: unknown = ...+ 类型收窄 |
| 函数重载顺序 | 通用签名在前 | 具体签名在前 |
| strictNullChecks | 未开启 | tsconfig 中开启 |
| 非空断言滥用 | obj!.prop | 先做类型守卫 |
| 类型断言错误 | data as WrongType | 类型守卫 + 类型断言 |
| Record<string, any> | 失去类型检查 | 定义明确的 interface |
| interface vs type | 混用 | 对象类型用 interface,联合/交叉用 type |
| never 穷举 | 不处理 default | const _exhaustiveCheck: never = val |