我有一个 Map ,一一对应。
TAP_TYPE.LOCAL
对应 TAP_LOCAL
,
TAP_TYPE.MAP
对应 TAP_MAP
。
我在函数中已经通过 switch 约束 test 的 v, 为什么 ts 还是推导 v 的类型是 TAP_LOCAL | TAP_MAP
?
或者对这样的例子,除了对 v 进行强制断言,有什么更好的写法吗?
enum TAP_TYPE {
'LOCAL',
'MAP',
}
interface TAP_LOCAL {
a: string;
}
interface TAP_MAP {
b: string;
}
type TapTypeMap = {
[TAP_TYPE.LOCAL]: TAP_LOCAL;
[TAP_TYPE.MAP]: TAP_MAP;
};
function test<T extends TAP_TYPE>(t: T, v: TapTypeMap[T]) {
switch (t) {
case TAP_TYPE.LOCAL:
return v.a; // 类型错误 类型“TAP_LOCAL | TAP_MAP”上不存在属性“a”。
case TAP_TYPE.MAP:
return v.b; // 类型错误 类型“TAP_LOCAL | TAP_MAP”上不存在属性“b”。
}
}
1
Opportunity 2023-09-07 00:09:19 +08:00
当 T=TAP_TYPE 有 t: TAP_TYPE, v: TAP_LOCAL | TAP_MAP 。
此时,t 和 v 没有任何关系,你对 t 再怎么判断也不应当影响 v 的类型,我觉得 ts 的推断没有任何问题。 我觉得断言已经是最好的方案了,接口上你可以选择使用重载代替泛型,避免 T=TAP_TYPE 这种情况,实现没啥好办法。 |
2
lqzhgood OP @Opportunity #1
我理解给参数 v TapTypeMap[T] 类型就是让 ts 知道 “一一对应的关系” switch 外层的 v 类型是 TAP_LOCAL | TAP_MAP 没错 但是通过 switch 缩小 t 的范围,关联到 TapTypeMap[T] 从而缩小 v 的范围我觉得也没问题吧~ 后来我想通过函数重载的方式去实现也一样报错了~ ```ts function test(t: TAP_TYPE.LOCAL, v: TAP_LOCAL); function test(t: TAP_TYPE.MAP, v: TAP_MAP); function test(t: TAP_TYPE, v: TAP_LOCAL | TAP_MAP) { switch (t) { case TAP_TYPE.LOCAL: return v.a; //报错 case TAP_TYPE.MAP: return v.b; //报错 } } ``` 顺着重载的思路搜到这个 2020 年的帖子 https://www.zhihu.com/question/402139008 问题类似,也没解决~ |
3
Opportunity 2023-09-08 18:16:19 +08:00 1
我的意思是,调用方这样写:
``` const t: TAP_TYPE = TAP_TYPE.LOCAL test(t, {b:'xx'}) ``` TS 不会报任何错误,运行时会炸。用函数重载可以在运行时就报错。 如果你硬要把接口搞成这样,就要想办法告诉 TS 两个参数的联系,比如这样写: ``` function test(...[t, v]: [t: TAP_TYPE.LOCAL, v: TAP_LOCAL] | [t: TAP_TYPE.MAP, v: TAP_MAP]) { switch (t) { case TAP_TYPE.LOCAL: return v.a; case TAP_TYPE.MAP: return v.b; } } ``` 但是说实话,太丑了,我更倾向于用 as |
4
Opportunity 2023-09-08 18:20:24 +08:00
|
5
chnwillliu 2023-09-11 18:15:05 +08:00 1
1. switch (t) 缩窄的是变量 t 的类型,并不会影响泛型 T 的范围,就算 T 真能跟随 case 缩窄变化,v:TapTypeMap[T] 也不能获得联动缩窄。T 是一个未知类型,extends 只是约束了这个未知的边界。
2. t 的类型是 TAP_TYPE 的子类型,导致 case 对 t 的类型缩窄失效,此时 t 的类型不再是可缩窄类型。TAP_TYPE 是 enum 类型,类似 union type ,可以缩窄,但从 enum 派生出去的类型不一定可缩窄。 enum TAP_TYPE { LOCAL, MAP, } interface TAP_LOCAL { a: string; } interface TAP_MAP { b: string; } type TapTypeMap = { [TAP_TYPE.LOCAL]: TAP_LOCAL; [TAP_TYPE.MAP]: TAP_MAP; }; function test<T extends TAP_TYPE>(t: T, v: TapTypeMap[T]): string; function test(t: TAP_TYPE, v: TapTypeMap[TAP_TYPE]): string { switch (t) { case TAP_TYPE.LOCAL: return (v as TapTypeMap[typeof t]).a; case TAP_TYPE.MAP: return (v as TapTypeMap[typeof t]).b; } } |
6
chnwillliu 2023-09-11 18:35:01 +08:00
#3 借助元组进行联动缩窄的方法很巧妙。
|
7
lqzhgood OP @chnwillliu #5
我不太明白第二点~ 请教第二点的意思是 `t extends TAP_TYPE` !== `t: TAP_TYPE` 么? 对于 TAP_TYPE 是一个 enum 类型的情况下,上述应该是相等的吧? 我好像找不出反例 |
8
chnwillliu 2023-09-12 18:29:34 +08:00 via Android
@lqzhgood
这里就要说到 ts 的 nominal type checking. type foo = 0 & {brand: 'foo'} 这里 foo 并不会是 never ,这是 ts 刻意为之的,虽然运行时不可能存在一种值满足这个类型。(但是可以在 ts 层面 as 啊) type bar = (0 & {brand: 'foo'}) extends 0 ? true : false; // true function test<T extends TAP_TYPE>(t: T, v: TapTypeMap[T]) T 实际有可能是 TAP_TYPE.LOCAL & {a:1} 或与任意其他 interface 的交叉类型。 |
9
lqzhgood OP 看到个 Ts 的 issue ,和这个问题相关 https://github.com/Microsoft/TypeScript/issues/22609
######################################################### function add(x:string,y:string):string; function add(x:number, y:number):number; //实现签名 对外不可见 function add(x:string|number, y: number|string): number | string{ if(typeof x === 'string'){ return x + ',' + y; }else { return x.toFixed() + (y as number).toFixed(); // 很不幸,ts 暂时不支持对函数重载后续参数的 narrowing 操作,如这里对 x 做了 type narrowing 但是对 y 没有做 narrowing ,需要手动的 y 做 type assert 操作 见 https://github.com/Microsoft/TypeScript/issues/22609 } } 作者:小电前端团队 链接: https://juejin.cn/post/6912309038743191559 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 |