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

355 天前
 makelove

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']
1257 次点击
所在节点    TypeScript
12 条回复
Iefty
355 天前
[key: string]: string | Date
enpitsulin
355 天前
因为索引类型定义就不应该和其他属性定义冲突啊,索引类型≼其他属性类型

```typescript
interface Bar {
name: string
[key: string]: Date | string
}
```
seashell2000
355 天前
use "Symbol" for name
makelove
355 天前
@Iefty 这样的话还不如强制转换呢,起换读属性的时候类型正确的
makelove
355 天前
@enpitsulin 这不是应不应该的问题,是现实中需要怎么办?
我要为一个第三方库写个类型定义,这个库有个类型就是有一些预定义的属性,还有其它动态扩展属性,这些个动态扩展属性就放在这个类型对象上
enpitsulin
355 天前
@makelove #5 用了索引就应该把类型定义和别的属性不冲突啊?实际问题的话,你要是有什么 X 就别问 Y
Belmode
350 天前
这段代码在 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
332 天前
因为对象的 key 可能来自运行时,ts 无法推断。试想如果取 obj[key] 那 ts 应该推断其类型为 Date 还是 string ? 如果算做 Date 那万一 key 的值在运行时其实是 'name' 呢?
makelove
331 天前
@chnwillliu ts 访问的时候可以区分 .xxx 访问和 [xxx] 索引访问,照你这么说的话为什么现在 bar.name 和 bar['xxx' as string] 可以得到正确的类型?
chnwillliu
331 天前
@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
331 天前
看到 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
331 天前
@chnwillliu 就是一个把 xml 转 js 的第三方。它的设定是把 xml 节点转成 js 对象,属性放在 $,文本放在 "_",子节点名直接作为属性。
所以呢,最终这个对象是
{ $: { ... }, _: 'node text', xxx: { ... }, yyy: { ... } }, 这些 xxx,yyy 都是动态的,只有 $ 和 _ 这二个属性可以保证是预定的类型。
用的时候 bar.$ 可以确定是个属性对象类型,不可能是别的。

所以,我需要的是用 $ 或 _ 这二个属性的时候是预设的类型,目前 ts 是可以做到的,因为它区分了用 .xxx 字面直接访问和 [xxx] 动态访问。当然了你说是误用也是有可能的,我不知道 ts 设计区分这二者是什么目的。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/954728

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX