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

TypeScript 的索引定义和原有字段类型冲突有什么技巧?

  •  
  •   makelove · 2023-07-07 08:13:41 +08:00 · 1425 次点击
    这是一个创建于 503 天前的主题,其中的信息可能已经有所发展或是发生改变。

    PlayGround

    type Bar = {
      name: string
    } & { // A: 这行为什么必需呢?官方文档里也没写清楚
      [key: string]: Date
    }
    
    declare const bar: Bar
    let a = bar.name // 正确的得到 string
    let b = bar['x'] // 正确的得到 Date
    
    const bar2: Bar = { name: 'xx' } // B: 这里为什么报错?
    
    const bar3 = { name: 'xx' } as Bar // C: 难道所有地方都要强制转换?
    let b2 = bar2['x']
    
    12 条回复    2023-07-31 17:08:09 +08:00
    Iefty
        1
    Iefty  
       2023-07-07 08:52:23 +08:00
    [key: string]: string | Date
    enpitsulin
        2
    enpitsulin  
       2023-07-07 09:25:19 +08:00   ❤️ 1
    因为索引类型定义就不应该和其他属性定义冲突啊,索引类型≼其他属性类型

    ```typescript
    interface Bar {
    name: string
    [key: string]: Date | string
    }
    ```
    seashell2000
        3
    seashell2000  
       2023-07-07 09:33:40 +08:00
    use "Symbol" for name
    makelove
        4
    makelove  
    OP
       2023-07-07 12:49:51 +08:00
    @Iefty 这样的话还不如强制转换呢,起换读属性的时候类型正确的
    makelove
        5
    makelove  
    OP
       2023-07-07 12:57:24 +08:00
    @enpitsulin 这不是应不应该的问题,是现实中需要怎么办?
    我要为一个第三方库写个类型定义,这个库有个类型就是有一些预定义的属性,还有其它动态扩展属性,这些个动态扩展属性就放在这个类型对象上
    enpitsulin
        6
    enpitsulin  
       2023-07-07 13:51:38 +08:00
    @makelove #5 用了索引就应该把类型定义和别的属性不冲突啊?实际问题的话,你要是有什么 X 就别问 Y
    Belmode
        7
    Belmode  
       2023-07-12 18:15:43 +08:00
    这段代码在 TypeScript 中并没有明显的语法错误,但它存在潜在的问题。

    问题在于类型 Bar 定义了一个属性 name 的固定类型为 string ,以及一个索引签名 [key: string]: Date ,允许任意字符串键名对应的值为 Date 类型。

    这种定义可能会导致类型不一致或产生意外行为。因为在 Bar 类型中,name 属性被指定为 string 类型,而索引签名允许任意字符串键名对应的值为 Date 类型。这样就引入了潜在的类型冲突。

    举个例子:

    typescript
    const bar: Bar = {
    name: 'John',
    age: new Date() // 错误,age 不是 Date 类型
    };
    在上述示例中,我们试图将一个具有 'name' 和 'age' 属性的对象赋值给 Bar 类型的变量 bar ,但是在 Bar 类型中并没有定义 age 属性,并且索引签名的值类型是 Date 。因此,这样的赋值将会导致类型错误。

    为了解决这个问题,你可以考虑`重新设计类型定义,确保属性和索引签名的类型一致`,或者`根据实际需求修改类型定义`。具体如何修改取决于你的使用场景和预期行为。

    GPT 说的很明确,你用法不对。
    chnwillliu
        8
    chnwillliu  
       2023-07-31 06:54:11 +08:00 via Android
    因为对象的 key 可能来自运行时,ts 无法推断。试想如果取 obj[key] 那 ts 应该推断其类型为 Date 还是 string ? 如果算做 Date 那万一 key 的值在运行时其实是 'name' 呢?
    makelove
        9
    makelove  
    OP
       2023-07-31 08:06:17 +08:00
    @chnwillliu ts 访问的时候可以区分 .xxx 访问和 [xxx] 索引访问,照你这么说的话为什么现在 bar.name 和 bar['xxx' as string] 可以得到正确的类型?
    chnwillliu
        10
    chnwillliu  
       2023-07-31 15:29:33 +08:00
    @makelove 用 intersection type 虽然规避了同一个 interface 内声明的字段类型必须兼容 index signature 的 TS 报错,但其实这是一种错误用法,不报错可能就像你说的,.xxx 和 索引访问在进行类型交叉的时候互不干扰。

    这个所谓的 index signature 必须兼容所有字段的设定,是为了规避在 bar['na'+'me'].getMonth() 这种场景下产生错误推断。

    这两种类型交叉后,更准确地,我觉得 bar.name 应该推断为 string & Date ,但 JS 中怎么构造一个既是 string 又是 Date 的变量呢。

    const bar2: Bar = { name: 'xx' as string & Date }

    你看这样就不报错了,说明右边的 literal object 必须同时满足 Bar 的两个类型才能赋值。但是因为强行 as ,运行时 bar['na'+'me'].getMonth() 仍然要报错。

    类型要安全那就只能加一层,把 name 摘出来:

    interface Bar {
    name: string;
    dates: {[key: string]: Date}
    }

    或者像 #3 说的,name 用 symbol 。

    module A {
    const name = Symbol('name')

    interface Bar {
    [name]: string;
    [key: string]: Date;
    }

    export const bar: Bar = {
    [name]: 'test',
    a: new Date,
    bar: new Date
    };
    }
    chnwillliu
        11
    chnwillliu  
       2023-07-31 15:43:03 +08:00   ❤️ 1
    看到 OP 说是给第三方 JS 添加类型,那说明它在运行时是有问题的

    declare const bar: Bar
    let key = 'name';
    bar[key] = new Date();
    typeof bar.name // ??

    key 如果是任意字符串,那它就有可能是 ‘name’。如果说运行时保证了 key 一定不是 name ,但 TS 不知道,你只能

    interface Bar {
    name: string;
    [key: string]: string | Date;
    }

    然后在每次 bar[key] 的时候告诉 TS 这里 key 不会是 name 所以类型一定是 Date 。
    makelove
        12
    makelove  
    OP
       2023-07-31 17:08:09 +08:00
    @chnwillliu 就是一个把 xml 转 js 的第三方。它的设定是把 xml 节点转成 js 对象,属性放在 $,文本放在 "_",子节点名直接作为属性。
    所以呢,最终这个对象是
    { $: { ... }, _: 'node text', xxx: { ... }, yyy: { ... } }, 这些 xxx,yyy 都是动态的,只有 $ 和 _ 这二个属性可以保证是预定的类型。
    用的时候 bar.$ 可以确定是个属性对象类型,不可能是别的。

    所以,我需要的是用 $ 或 _ 这二个属性的时候是预设的类型,目前 ts 是可以做到的,因为它区分了用 .xxx 字面直接访问和 [xxx] 动态访问。当然了你说是误用也是有可能的,我不知道 ts 设计区分这二者是什么目的。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1540 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 17:06 · PVG 01:06 · LAX 09:06 · JFK 12:06
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.