【问题标题】:How to specify the generic typing when instantiating a class which allows a value of type or a value of a derived type如何在实例化允许类型值或派生类型值的类时指定泛型类型
【发布时间】:2022-01-20 07:34:24
【问题描述】:

为什么这个 TypeScript 代码...

interface Base {
    prop1: string
}

interface Derived extends Base {
    prop2: string
}

class Collection<T> {
  items: T[] = []
  add(item: T): T {
    this.items.push(item)
    return item
  }
}

const col = new Collection<Base>()
col.add({
    prop1: 'value1',
    prop2: 'value2'
})

// Note this work around, which demonstrates that this is perfectly workable without the typing
console.log(
  (
    col.add({
      prop1: 'value1',
      prop2: 'value2'
    } as any) as any
  )
  .prop2
)

(见here

...产生这个错误...

Argument of type '{ prop1: string; prop2: string; }' is not assignable to parameter of type 'Base'.
  Object literal may only specify known properties, but 'prop2' does not exist in type 'Base'. Did you mean to write 'prop1'?

更具体地说,我如何指定我想创建一个Collection 的实例,它接受Base 类型的项目以及任何和所有派生接口类型(但没有其他类型)而不指定所有派生类型的联合?

根据目前的回复,我猜想这在当前 (4.5.4) 版本的 TypeScript 中是不可能的。

【问题讨论】:

  • 呃,你在做什么把string 作为WeakMap 的键?这会给你一个运行时错误,TypeScript 也不喜欢它(从 string 扩大到对象包装器 String 不是解决这个问题的方法)。由于您的问题显然与此类事情无关,您可以edit 删除它吗?你可以只使用普通的Map 而不是WeakMap,对吧?
  • TypeScript 不喜欢string,但可以使用String。在这里使用字符串似乎有助于使示例保持简单。
  • 您明白您的示例虽然简单,但在运行时会爆炸,对吗?你希望这种情况发生吗?如果没有,你能解决它吗?如果是这样,为什么?如果你不关心,你能修复它吗(这样其他人就不必弄清楚它发生了什么)?
  • 无论如何this 是我在这里回答的方法,但它可能与Alex Wayne's answer 的区别不足以打扰。祝你好运!
  • 已更新。希望这个新的例子更受欢迎。

标签: typescript


【解决方案1】:

完全跳过代码示例的“为什么”,我认为这与WeakMap 或扩展接口完全无关。所以我要大大地简化这个问题:

type A = { str: string }
const test1: A = { str: 'string', num: 123 }
// Type '{ str: string; num: number; }' is not assignable to type 'A'.
//  Object literal may only specify known properties, and 'num' does not exist in type 'A'.(2322)

Typescript 会因错误而做出很多判断。在这里它决定这可能是一个错误,因为您直接将对象文字分配给缺少某些文字属性的类型。这会导致类型系统“忘记”那些额外的属性,从而导致它们无法访问。

但是,这很好用:

type A = { str: string }
const test2Data = { str: 'string', num: 123 }
const test2: A = test2Data

这是因为test2Data现在是自己的值和类型,可以独立使用。 testData.num 的类型为 number,但 test2.num 不存在(根据类型)。

Playground


此处适用完全相同的规则,并且没有问题:

const map = new WeakMap<String, Base>()
const data = {
    prop1: 'value1',
    prop2: 'value2'
}
map.set('key1', data)

Playground


TL;DR:当你将一个对象字面量分配给一个类型时,立即会丢失该字面量中的一些属性,打字稿认为这是一个错误,因为你永远无法在初始分配。


如果您真的想捕获额外的未知属性,则应该以类型安全的方式进行。就像泛型一样:

function makeThing<T extends { str: string }>(data: T): T {
  return data
}

const thing = makeThing({ str: 'string', num: 123 })
thing.num // safe

Playground


您的问题与我第一次回答时的代码完全不同。

您可以通过将add() 函数设为泛型来“解决”这种情况。

class Collection<T> {
  items: T[] = []
  add<U extends T>(item: U): U {
    this.items.push(item)
    return item
  }
}

现在,add() 不再接受 T,而是接受 TT 的子类型。这告诉 Typescript 额外的属性不会被丢弃,因为它们将由方法返回类型返回。

测试一下:

const col = new Collection<Base>()
const addedItem = col.add({
    prop1: 'value1',
    prop2: 'value2'
})
addedItem.prop1 // works
addedItem.prop2 // works

col.items[0].prop1 // works
col.items[0].prop2 // type error
;(col.items[0] as Derived).prop2 // works

但是,这又不是一个好主意。因为如果您实际上没有添加prop2,因为add 方法不需要它,那么您就无需防范假设该属性存在。

故意将数据填充到缺少该数据属性的类型中,然后用不安全的强制转换将其取出,这对我来说似乎是一个糟糕的计划。

Playground

【讨论】:

  • 没有类型安全,你不能,这就是为什么你必须在那里转换为any。这就是重点。它存在于实际值中,但它不存在于类型中。这是因为 Typescript 编译为 javascript 而存在的那些奇怪的事情之一,而 javascript 没有类型。 Typescript 的存在是为了让 javascript 更加严格。这是严格的一部分。
  • 让我再试一次:您绝对可以在初始分配后访问这些属性:console.log((map.get('key1') as Derived).prop2)
  • 没有类型安全,你不能。使用as 进行类型转换不是类型安全的。它告诉编译器你,程序员,相信这个值实际上是编译器认为的更具体的子类型,你,程序员,可能是错的,Typescript 不会检查你。一个简单的as 可以静默破坏您的程序的示例:tsplay.dev/m3aYyw
  • 好吧,从技术上讲,我没有要求类型安全。即使没有类型安全,也有一些方法可以使您的代码不中断 (tsplay.dev/wX2pQm)。此外,使用泛型正是我想要的——它甚至在问题的标题中!但是我需要能够在实例化一个新类时指定扩展类型,而不是在定义一个函数时。无论如何在问题的示例中使用泛型?
  • 我已经更新了答案的结尾,以实际回答您更新后的问题。至少我认为是的。不过还是不推荐这个。
猜你喜欢
  • 1970-01-01
  • 2011-05-24
  • 1970-01-01
  • 2015-12-09
  • 1970-01-01
  • 2019-01-20
  • 2013-09-18
  • 1970-01-01
  • 2012-09-08
相关资源
最近更新 更多