Typescript - 逐步扩展对象的类型

6
我正在尝试使用TypeScript实现以下目标:
let m: Extendable
m.add('one', 1)
// m now has '.one' field
m.add('two', 2)
// 'm' now has '.one' and '.two' fields

我熟悉通过以下方式在TS中返回扩展类型:

function extend<T, V>(obj: T, val: V): T & {extra: V} {
    return {
        ...obj,
        extra: val
    }
}

现在,我的情况有两个问题:

1)对象m在调用add()后需要更新其类型以反映添加新字段的情况。

2)新字段的名称是参数化的(不总是extra,例如)。

第一个问题可以通过使用类定义并以某种方式使用TypeThis实用程序重新调整类型来解决,但我找不到足够的文档说明如何使用它。

欢迎任何帮助或指导。谢谢!


可以合理地假设add()的第一个参数是字面量,而不是变量。 - Dragan Okanovic
@MaciejSikora - 我知道有更好的方法来完成我所做的小部分。 :-) - T.J. Crowder
不错的@MaciejSikora。我正在尝试从这里找到答案https://stackoverflow.com/questions/50899400/how-can-we-type-a-class-factory-that-generates-a-class-given-an-object-literal,因为它更新了类型,但是有太多其他代码,很难看出什么是重要的。 - Dragan Okanovic
1
在TypeScript 3.7+中,也有断言函数,可以解决这个问题,但是存在一个令人讨厌的警告,涉及到类型注释(let e = new Extendable()是行不通的,但是let e: Extendable = new Extendable()是行得通的),因此我不确定是否值得这样做。 - jcalz
显示剩余4条评论
1个回答

5
TypeScript 3.7引入了断言函数,可用于缩小传入参数甚至this的类型。 断言函数看起来有点像用户自定义类型保护,但在类型谓词之前添加asserts修饰符。以下是实现Extendable作为一个带有add()断言方法的类的示例:
class Extendable {
    add<K extends PropertyKey, V>(key: K, val: V): asserts this is Record<K, V> {
        (this as unknown as Record<K, V>)[key] = val;
    }
}

当您调用 m.add(key, val) 时,编译器会断言 m 将具有类型为 key 的键和相应类型为 val 的值的属性。以下是使用方法:
const m: Extendable = new Extendable();
//     ~~~~~~~~~~~~ <-- important annotation here!
m.add('one', 1)
m.add('two', 2)

console.log(m.one.toFixed(2)); // 1.00
console.log(m.two.toExponential(2)); // 2.00e+0

那一切都如你所预期的那样运作。在调用 m.add('one', 1) 后,你可以无需编译器警告地引用 m.one
不幸的是,有一个相当重要的限制; 断言函数仅在具有明确定义类型时才起作用。根据相关拉取请求,"这个特定规则的存在是为了控制潜在断言调用的控制流分析,以避免循环触发进一步的分析。"
这意味着以下内容是错误的:
const oops = new Extendable(); // no annotation
  oops.add("a", 123); // error!
//~~~~~~~~ <-- Assertions require every name in the call target to be declared with
// an explicit type annotation.

唯一的区别在于,oops 的类型被推断为 Extendable,而不像 m 一样被注释为 Extendable。当你调用 oops.add() 时会出现错误。根据您的使用情况,这可能并不重要,也可能是无法继续的问题。

好的,希望能帮到你;祝你好运!

链接到代码


这有点像魔法。但它确实实现了我想要的。 我不明白为什么它不断言 Record<K, V> & {[key]: V} 或类似的东西,即类型何时会扩展一个额外的字段?我可以看到它稍后被分配,但类型何时改变以及如何/为什么? 此外,对于我的用例,警告并不影响我。 - Dragan Okanovic
我不确定我理解问题的意思。add() 的返回类型 this is Record<K, V> 意味着 “如果 this.add() 返回,那么 this 的类型将被缩小为 Record<K, V>”。所以 m 开始时是 Extendable,然后您调用了 m.add("one",1),在该行之后的任何代码中,m 将为 Extendable & Record<"one", number>。之后你再调用 m.add("two", 2),在该行之后的任何代码中,m 将为 Extendable & Record<"one",number> & Record<"two", number>add() 的实现必须实际添加字段,否则断言就是一个谎言。 - jcalz
我理解你所写的内容以及其逻辑,除了两种类型连接发生的地方(Extendable & {...}Extendable & {...} & {...})。Assert只是将其强制转换/确保为Record<K,V>,而不是whatever is previous & Record<K,V>。或者说,仅仅说“这是Record<K,V>”就可以了吗? - Dragan Okanovic
1
是的,我认为在某些情况下交集会自动发生。如果这让你感到困扰,你可以将其改成 this is this & Record<K, V>,这样就会变得明确。 - jcalz
好的,我明白了。我认为asserts this is Record<K,V>不应该被理解为"this是类型Record<K,V>",而应该理解为"this在某些部分上满足Record<K,V>"。这类似于类型守卫通常的做法——它们不完全匹配,但确保可用的接口类型。 - Dragan Okanovic
@AbstractAlgorithm - 是的,“is”不一定意味着“仅是”。如果我有FooBar extends Foo以及const b: Bar,那么b就是Foo。它不仅仅是Foo,但它确实是Foo - T.J. Crowder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接