V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
lqzhgood
V2EX  ›  TypeScript

[求助] 函数中使用泛型缩小参数类型

  •  
  •   lqzhgood · 2023-09-06 10:02:53 +08:00 · 1082 次点击
    这是一个创建于 435 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我有一个 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”。
        }
    }
    
    9 条回复    2023-09-30 22:16:35 +08:00
    Opportunity
        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 这种情况,实现没啥好办法。
    lqzhgood
        2
    lqzhgood  
    OP
       2023-09-07 16:18:32 +08:00
    @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
    问题类似,也没解决~
    Opportunity
        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
    Opportunity
        4
    Opportunity  
       2023-09-08 18:20:24 +08:00
    https://imgur.com/a/dVFMfeW

    不过 intellsense 说明 TS 团队推荐的写法就是这个
    chnwillliu
        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;
    }
    }
    chnwillliu
        6
    chnwillliu  
       2023-09-11 18:35:01 +08:00
    #3 借助元组进行联动缩窄的方法很巧妙。
    lqzhgood
        7
    lqzhgood  
    OP
       2023-09-12 09:54:13 +08:00
    @chnwillliu #5

    我不太明白第二点~
    请教第二点的意思是 `t extends TAP_TYPE` !== `t: TAP_TYPE` 么?
    对于 TAP_TYPE 是一个 enum 类型的情况下,上述应该是相等的吧? 我好像找不出反例
    chnwillliu
        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 的交叉类型。
    lqzhgood
        9
    lqzhgood  
    OP
       2023-09-30 22:16:35 +08:00
    看到个 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
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1924 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 16:28 · PVG 00:28 · LAX 08:28 · JFK 11:28
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.