TypeScript根据属性值推断对象类型 无需类型断言方法
问题背景
现有简化TS代码如下,逻辑上已经可以确定category === 'timeout'时option为TimeoutOption类型,但TS编译报错,必须加as类型断言才能通过,需要在不使用类型断言的前提下修复问题,支持完整重构代码。
interface TimeoutOption { category: 'timeout' time: number } interface UserOption { category: Parameters<typeof addEventListener>[0] } type MyDelayOptions = Array<TimeoutOption | UserOption> function delay(options: MyDelayOptions) { for (const option of options) { if (option.category === 'timeout') { timeout = setTimeout(() => { // not relevant }, option.time) // 必须做(option as TimeoutOption)类型断言才能消除错误 } } }
报错根因
TS判别式联合自动收窄的前提是,各类型的判别字段(这里是category)的字面量类型互斥。这里报错的核心原因是:
TimeoutOption的category类型是字面量'timeout'- 对重载函数使用
Parameters这类工具类型时,TS默认只会取最后一个重载签名的类型,addEventListener最后一个重载的第一个参数类型是string,也就是说UserOption的category类型是string,本身就包含'timeout'这个值。 - 此时TS无法确定
category === 'timeout'的分支里option一定是TimeoutOption——毕竟UserOption的category是字符串,完全可能取值为'timeout',而UserOption上没有time属性,就会抛出类型错误。
可选解决方案
方案1:修正类型定义让判别字段互斥(最推荐,无额外运行时开销)
从事件名类型中排除'timeout',让两个接口的category类型完全互斥,TS就能自动完成类型收窄,不需要任何额外判断:
interface TimeoutOption { category: 'timeout' time: number } // 取出DOM事件名联合类型,排除'timeout'保证和TimeoutOption的category互斥 type DomEventName = Exclude<keyof DocumentEventMap, 'timeout'> interface UserOption { category: DomEventName } type MyDelayOptions = Array<TimeoutOption | UserOption> // 原代码漏了timeout变量声明,补上 let timeout: number | undefined function delay(options: MyDelayOptions) { for (const option of options) { if (option.category === 'timeout') { // TS自动收窄option为TimeoutOption,访问option.time无报错 timeout = setTimeout(() => { // 业务逻辑 }, option.time) } } }
如果业务场景里
UserOption确实需要支持传入'timeout'作为事件名,不要用这个方案,选下面两种。
方案2:使用in操作符做属性检查(改动最小)
不需要修改原有类型定义,只需要在判断条件里加一层属性存在性校验,TS会根据in操作符自动收窄类型:
interface TimeoutOption { category: 'timeout' time: number } interface UserOption { category: Parameters<typeof addEventListener>[0] } type MyDelayOptions = Array<TimeoutOption | UserOption> let timeout: number | undefined function delay(options: MyDelayOptions) { for (const option of options) { // 增加time属性存在判断,TS会自动识别当前分支的option为TimeoutOption if (option.category === 'timeout' && 'time' in option) { timeout = setTimeout(() => { // 业务逻辑 }, option.time) } } }
方案3:自定义类型守卫(适合复杂判断逻辑场景)
如果类型判断逻辑比较复杂,可以封装成独立的类型守卫函数,显式告诉TS收窄规则:
interface TimeoutOption { category: 'timeout' time: number } interface UserOption { category: Parameters<typeof addEventListener>[0] } type MyDelayOptions = Array<TimeoutOption | UserOption> let timeout: number | undefined // 类型守卫函数,返回值用is语法标记类型 function isTimeoutOpt(option: MyDelayOptions[number]): option is TimeoutOption { return option.category === 'timeout' && 'time' in option && typeof option.time === 'number' } function delay(options: MyDelayOptions) { for (const option of options) { if (isTimeoutOpt(option)) { // 分支内option自动收窄为TimeoutOption timeout = setTimeout(() => { // 业务逻辑 }, option.time) } } }
内容的提问来源于stack exchange,提问作者v-moe
相关产品推荐
相关产品推荐

