【问题标题】:TypeScript: Override Generic Types in Method<T> that inherit from a Class<T> fallbackTypeScript:覆盖 Method<T> 中继承自 Class<T> 回退的泛型类型
【发布时间】:2021-12-28 15:27:24
【问题描述】:

我准备了一个 Angular 服务的简化示例,它带有多个允许通过方法覆盖的通用类型。我在这里面临的问题是= vs extends 并在参数中输入。出于某种原因,= 非常适合返回类型,但参数类型需要 extends

class Service<
    RequestBody,
    ResponseBody,
    CreateRequestBody = RequestBody,
    CreateResponseBody = ResponseBody,
>
{
    public createOne<
        $CreateRequestBody extends CreateRequestBody,
        $CreateResponseBody = CreateResponseBody
     >(body : $CreateRequestBody) : $CreateResponseBody
    {
        return null;
    }

    public createTwo<
        $CreateRequestBody = CreateRequestBody,
        $CreateResponseBody = CreateResponseBody
    >(body : $CreateRequestBody) : $CreateResponseBody
    {
        return null;
    }
}

以下是反映问题的示例,我想在发现问题后使用createTwo() 方法。此时它不会验证 body 的类型。

const service: Service<{ foo: string }, void> = new Service<{ foo: string }, void>();

// working
service.createOne({
    foo: 'test'
});

// not fine as the types are not compatible
service.createOne<void, void>({
    bar: 'test'
});

// not fine as everything can be set for body - typing not working for some reason
service.createTwo({
    bar: 'test'
});

// fine as the local override works
service.createTwo<{ bar: 'test' }, void>({
    bar: 'test'
});

这里是TypeScript Playground,看看编译器做了什么。

【问题讨论】:

  • 您的游乐场链接已损坏;你能修好吗?
  • 你知道=是什么吗?这是default,而不是约束。无论如何,我不确定所有泛型类型参数的目的是什么。如果我删除了课堂上除前两个之外的所有内容,我会得到this code,这似乎工作得很好。这是我的建议,除非你可以让 minimal reproducible example 演示一个需要额外类型参数的用例。
  • 我修复了操场链接,谢谢
  • 另请注意,您的示例代码在return null 处有错误;您可能应该删除或抑制这些错误,以免分散您的问题。如果您不关心实现,您可以写return null! 使其成为never。或者你可以做点别的。
  • 糟糕,抱歉...改用this

标签: typescript generics types


【解决方案1】:

首先,让我们删除Service 上的后两个类型参数,因为它们似乎除了作为现有请求/响应主体类型的“副本”之外没有其他用途:

class Service<Rq, Rs> { /* ... */ }

请注意,我使用 RqRs 分别表示原始请求和响应正文类型,因为我倾向于遵循(诚然有时必须阅读)通用类型参数短一的约定 -或两个字符名称,以便它们可以与特定类型区分开来。


所以对于create() 方法,您通常希望body 类型为Rq,返回类型为Rs。但是,您还希望能够覆盖这些类型,方法是在调用时显式指定类型,例如 create&lt;NewRequestType, NewResponseType&gt;(body),它的 body 的类型为 NewRequestType 并返回NewResponseType

免责声明:在这一点上,我想指出,很难想象这个函数的任何实现可能是类型安全的,因为运行时的实现对编译器传入的类型一无所知。例如,create&lt;string, number&gt;("hey") 应该返回一个 numbercreate&lt;string, boolean&gt;("hey") 应该返回一个 boolean,但是这两个调用都编译为 JavaScript create("hey"),因此即使在原则上也无法在所有情况下都是准确的。似乎您只是想让调用者使用这些类型参数,就好像他们在使用 type assertions,例如 create("hey") as numbercreate("hey") as boolean,这至少是一种明确且众所周知的方式来确认您正在放弃关于类型安全。但是为了进步,我会考虑超出范围的实现的类型安全,我不会进一步阐述这一点。


因此,您需要像&lt;RqO, RsO&gt;(body: RqO) =&gt; RsO 这样的通用调用签名,其中RqORsO 是所需的覆盖类型,但如果您不指定它们,它们将分别回退到RqRs。但是您将遇到的最大障碍是,如果您不指定类型参数,编译器将尝试infer them。无论您为body 传递什么,都会导致编译器为RqO 推断该类型,因为bodyRqO 的一个推断站点。此外,如果您碰巧在返回类型具有预期的contextual 类型的地方调用create(),编译器将为RsO 推断该类型,因为上下文返回类型是RsO 的推断站点。这不是你想要的;您想禁用RqORsO 的类型推断。如果你能说不应该使用body 来推断RqO 并且不应该使用上下文返回类型来推断RsO,那就太好了。

没有“关闭”推理站点的官方方法,但在 microsoft/TypeScript#14829 有一个功能请求,它要求以某种方式实现像 NoInfer&lt;T&gt; 这样的类型函数,最终计算结果为 T 但不能使用推断T。幸运的是,有一些建议至少适用于某些用例。我常用的来自this comment

type NoInfer<T> = [T][T extends any ? 0 : never];

以防万一:这里我们使用的事实是,依赖于未指定泛型的distributive conditional types延迟,所以T extends any ? 0 : never 直到T 被修复后才会评估。无论T 变成什么,T extends any ? 0 : never 最终都会评估为0,而[T][0] 只是T(因为你是indexing 成为单个元素tuple type 的第一个元素)。

这意味着我们可以这样写create()

    public create<RqO = Rq, RsO = Rs>(
        body: NoInfer<RqO>
    ): NoInfer<RsO> {
        return null!;
    }

我们使用Rq 中的generic parameter defaultsRs 来表示RqORsO,所以当没有指定类型参数并且推理失败时(肯定会失败),它们会回退到默认值。


那么,让我们试试吧:

const service = new Service<{ foo: string }, string>();

首先,我们不会指定请求和响应类型,希望编译器从service 回退到RqRs 类型:

const ret = service.create({
    foo: 'test'
}).toUpperCase(); // okay

service.create({
    bar: 'test'    }); // error, {bar:string} is not {foo: string}

const ret2: number = service.create({
    foo: 'test'
}); // error, number is not string

看起来不错。现在我们指定类型,希望编译器会使用它们,而不是来自service的那些:

const ret3 = service.create<{ bar: string }, number>(
    { bar: "test" }).toFixed(2); // okay

const ret4 = service.create<{ bar: string }, number>(
    { foo: "test" }
); // error, {foo:string} is not {bar: string}

const ret5: string = service.create<{ bar: string }, number>(
    { bar: "test" }

看起来也不错。所以这是一个可行的解决方案。


请注意,还有其他方法可以获取非推理类型参数的用法。你可以add additional type parameters 代替:

        public create<
            RqO = Rq,
            RsO = Rs,
            RqI extends RqO = RqO,
            RsI extends RsO = RsO
        >(body: RqI): RsI {
            return null!;
        }

这是有效的,因为编译器将使用body 来推断RqI(认为“隐式”覆盖)和返回类型来推断RsI,但约束是RqI extends RqORsI extends RsO,反之亦然.所以RqIRsI 的推论不会影响RqORsO。您可以验证其行为是否类似。我不会更详细地介绍,但这种方法有时在 NoInfer&lt;T&gt; 不起作用的情况下有效。


Playground link to code

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-08-13
    • 1970-01-01
    • 1970-01-01
    • 2013-02-06
    • 2015-11-18
    • 1970-01-01
    • 2020-08-22
    相关资源
    最近更新 更多