【问题标题】:Understanding Generic Constraint Extends in TypeScript理解 TypeScript 中的泛型约束扩展
【发布时间】:2019-08-04 03:32:12
【问题描述】:

我正在尝试创建一个通用函数,它接受基类的子类型,并返回一个解析为指定类的实例的承诺。这个例子展示了我想要实现的目标:

class foo {}
class bar extends foo {}
const someBar = new bar();
function foobar<T extends foo>(): Promise<T> {
  return new Promise<T>(resolve => resolve(someBar)); // this doesn't compile
}

我意识到 TypeScript 正在使用结构类型,因此在这个简单的示例中它将接受 T 的任何类型。但我不确定为什么它不允许我返回 someBar 的值。

有没有办法实现我在这里尝试做的事情?谢谢!

我得到的编译器错误是:

const someBar: bar
Argument of type 'bar' is not assignable to parameter of type 'T | PromiseLike<T> | undefined'.
  Property 'then' is missing in type 'bar' but required in type 'PromiseLike<T>'.ts(2345)
lib.es5.d.ts(1393, 5): 'then' is declared here.

更新

根据要求,这里有一些关于我要完成的工作的附加信息。这段代码编译得很好(我在 foo 和 bar 中添加了函数,只是为了在结构上区分它们):

class foo {
  f() {}
}
class bar extends foo {
  b() {}
}
const someBar = new bar();
function foobar(): Promise<foo> {
  return new Promise<foo>(resolve => resolve(someBar));
}
foobar().then(result => {
  const myBar: bar = result as bar;
  console.log(myBar);
});

我试图避免需要向下转换 promise-const myBar: bar = result as bar 的多态结果,如下所示:

class foo {
  f() {}
}
class bar extends foo {
  b() {}
}
const someBar = new bar();
function foobar<T extends foo>(): Promise<T> {
  return new Promise<T>(resolve => resolve(someBar));
}
foobar<bar>().then(result => {
  const myBar: bar = result;
  console.log(myBar);
});

TS 正确推断结果是一个条形,但它不允许我在函数中返回 someBar。

我已经能够在我的泛型类方法处理this 的类中使用多态this 来实现这一点 - 但这里我不在课堂上。

更新 2

实际上,这不需要承诺来说明我在做什么。在这里,我进一步简化了(并省略了 foo 和 bar 的定义,因为它没有改变):

function foobar<T extends Foo>(): T {
  return someBar;
}
const mybar: Bar = foobar<Bar>();

而且,这就是我在“纯 javascript”中所做的:

var someBar = new bar();
function foobar() {
  return someBar;
}
var myBar = foobar();

如您所见,我所做的只是微不足道的。我只是想在不向下转换的情况下进行多态类型检查。

【问题讨论】:

  • 这是对泛型的常见误解。通用参数由函数的调用者决定。这些函数无法决定泛型参数是什么。通过在此处返回someBar,您的函数假定Tbar,它不一定是。
  • 我想我不明白约束的作用,如果它不约束类型 =)
  • 正在限制类型。您不能使用 any T 调用该方法。泛型约束不约束方法实现(它实际上允许你在实现中做更多事情),而是调用者
  • @EricS 如果你用纯 javascript 编写这个函数,代码会是什么? (您提供了描述,但它与您提供的代码并不真正匹配)如果我们知道 javascript,我们也许可以帮助您向其中添加类型信息。
  • 嗨@NicholasTower - 我不确定纯javascript在这里会有所帮助,因为我只是使用一个简单的承诺 - 但试图实现多态静态类型安全。但我确实在上面编辑了我的问题,并展示了一个有效的非通用版本。这个版本要求我向下转换结果。我试图避免沮丧。这有意义吗?

标签: typescript generics


【解决方案1】:

您提供的代码不是通用函数,因此您在尝试将其键入时走错了路。

泛型函数需要处理多种数据类型,同时仍能够指定函数不同部分的类型之间的关系。例如,您可以指定函数传递某种类型,然后返回相同类型的数组

function wrapInArray<T> (input: T): T[] {
   return [T];
}
// To be used like:

const result1 = wrapInArray(1234); // result1 is a number array;
const result2 = wrapInArray('hello'); // result2 is a string array;

在上面的例子中,T 是任何类型的占位符。它的目的是告诉 typescript 事物是如何相互关联的。您可能不提前知道 T 是什么,但您仍然可以说输入和输出是相互关联的。


在许多情况下,您希望更具体地使用泛型。这是因为如果你不具体,那么你就不能在函数内部做很多事情,因为你不知道你正在使用什么类型。通过使用extends,您可以指定该类型必须具有某些属性。这将禁止任何人使用没有这些属性的任何东西调用您的函数,然后允许您在函数内随意使用这些属性。例如:

function getLength<T extend { length: number }>(input: T): number {
  // I can access input.length because even though i don't know what input's type is,
  //   i do know that it's illegal to call this function with anything that doesn't
  //   have a length property.
  return input.length;
}

// to be used like:

const len1 = getLength([1, 2, 3]);
const len2 = getLength({ length: 5 });

你的函数只做一件事:返回一个Promise&lt;bar&gt;。您需要指定的类型之间没有关联,并且没有可能调用该函数的方法,这会导致它返回除Promise&lt;bar&gt; 之外的其他内容。所以正确的类型是:

const someBar = new bar();
function foobar(): Promise<bar> {
  return new Promise(resolve => resolve(someBar));
}

更新:

function foobar<T extends Foo>(): T {
  return someBar;
}
const mybar: Bar = foobar<Bar>();

同样,此代码不是泛型函数的示例。它总是返回一个酒吧,而不是其他任何东西。应该这样输入:

function foobar(): Bar {
  return someBar;
}


const myBar = foobar(); 
// You could add the type explicitly if you prefer, but it's not necessary:
// const myBar: Bar = foobar();
// And since anything that's a Bar necessarily has all the properties of a Foo, you can also do:
const myFoo: Foo = foobar();

【讨论】:

    【解决方案2】:

    好的,我非常感谢这里的所有反馈 - 但我已经对此进行了研究,这里有一点值得说明。首先 - 我很惊讶我的例子如此关注。我故意提供了一个非常简单的例子。这就是所谓的“最小再现”——我的简单示例显示了一个编译器错误,其中像我这样的人可能(当然错误地)认为返回类型是受约束的泛型类型的有效多态子类型。同样,大多数情况下的回答都没有错——但它们并没有完全解决这个问题。我将尝试在此处提供更全面的图片。

    我将稍微分解一下我的最后一个示例:

    class Foo { f() {} }
    class Bar extends Foo { b() {} }
    var someBar = new Bar();
    function foobar<T extends Foo>(): T {
      return someBar;
    }
    const mybar: Bar = foobar<Bar>();
    

    正如其他人所指出的,语法function foobar&lt;T extends Foo&gt;() 指定了一个具有类型约束的泛型函数。见TypeScript Generics - Constraints

    虽然在技术上并不准确,但您可以说 foobar 的类型参数必须是 Foo 的子类。从技术上讲,TypeScript 只是简单地验证 T 的静态定义在结构上与 Foo 兼容。编译器尽职尽责地使用此类型信息来确保您只能使用某种类型调用此泛型,无论出于何种意图和目的,它都是 Foo(就像我一样)。

    有趣的是,我的 Swift 直觉让我误入歧途的是,编译器会立即忘记函数内部的这种类型信息。当它看到 T 的返回类型(或函数中 T 的任何其他用途)时,它不再将其视为“T 是 Foo 的子类”,而是“已知宇宙中的任何类型”。不管我的例子多么可疑,如果编译器记得不要忘记这个类型信息,它应该很高兴允许我从函数返回一个 Bar - 因为“Bar extends Foo”就像函数的签名所说的那样。

    我知道——我为什么会想要这样的东西,对吧?没有人会这样做......(好吧,除了在 Swift 中,但是,没关系)。一位响应者甚至说这不是通用功能。我承认,我只是在寻找一点语法糖。我想写foobar&lt;Bar&gt;() 而不是foobar() as Bar。但是向下转换与经过类型检查的函数调用之间存在一些差异。在完整的上下文中,这可能是一个非常小的好处,但那里可能有一些好处。但是,我试图这样做是有原因的。这是我的实际代码;

    class GlobalResolver extends CachingResolver {
      protected _resolve<T extends MdkObjectBase>(
        keyPath: string[]
      ): Promise<T> {
        const globName = keyPath.join("/");
        return Global.load(this._app, this._loadContext, `./Globals/${globName}`);
      }
    }
    

    这不会按原样编译。就像我上面的return someBar 一样,TS 不记得 T 被限制为在结构上与 MdkObjectBase 兼容。它的行为就像 T 可以是任何东西,因此我无法从 Global.load (a Promise) 分配结果。

    有趣的是,如果 _resolve 返回 GlobalResolver 的子类,我可以将其指定为返回类型:resolve(keyPath: string): this。这利用了 TS 中的“多态 this”。它不是通用的,但它允许我从基类返回一个子类,而调用者不必向下转换。但是我需要使用除此之外的其他类型来执行此操作,而且似乎没有办法做到这一点。

    总结一下:TypeScript 泛型中的类型约束约束函数可以接受的类型。但它不会影响函数中的泛型类型——TypeScript 选择忘记该信息。我相信它会很有用,就像多态这对于防止需要向下转换一样有用。而且这不是史无前例的(我很确定 Swift 中的通用约束正如我所描述的那样有效)。但是,唉,这不是 TS 的创造者所设想的,我相信他们的选择是有原因的。我只能沮丧了。

    【讨论】:

    • it no longer treats this as "T that is a subclass of Foo" but rather "Any type in the known universe" 抱歉,这是错误的。在函数内部,您可以使用 Foo 的任何方法。你可以这样做,因为它绝对是一个 Foo,并且没有更广泛的。但它也可能比 foo 更更窄,所以你不能使用任何需要更窄类型的东西(除非你先进行类型检查)。因此,尝试返回 Bar 比 Foo 更窄,打字稿说“等等,有人可能会用 &lt;Foo&gt; 调用它,在这种情况下,你返回的东西比他们预期的更窄”
    • 查看问题的另一种方式:有人可以使用 T = Qux 调用该函数。 Qux 从 Foo 扩展而来,拥有一个 Foo 没有的额外属性(Bar 也没有)。您列出的类型指定由于 T = Qux,将返回 Qux。 (或 Promise&lt;Qux&gt; 取决于我们使用的示例)。但是您的代码无法保证该额外属性是返回对象的一部分。您将返回 Bar 上的所有额外属性,但这对使用 Qux 调用它的人没有帮助。
    • it should have happily allowed me to return a Bar from the function- because "Bar extends Foo" - 你看,“Bar extends Foo”还不够好。编译器试图确保BarT 兼容,而不是Foo,并且看不到任何允许它假设的东西。完全可以像这样使用foobarfoobar&lt;{baz: number}&gt;(),像这样:foobar&lt;{quux: {baz: number}}&gt;(),等等。 T,虽然受到限制,但仍然是一个通配符,它​​可以是任何东西,你不能只返回 bar 并期望它与 T 兼容。
    • 好吧,这些 cmets 帮助很大...你们是对的。我正在尝试将泛型用作句法“宏” - 并让它接受我的知识,即演员表是合适的。在代码中明确说明这一点更有意义。
    猜你喜欢
    • 1970-01-01
    • 2018-12-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-04-09
    • 2016-11-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多