【问题标题】:TypeScript require generic parameter to be providedTypeScript 需要提供泛型参数
【发布时间】:2021-07-14 04:54:56
【问题描述】:

我有以下功能:

async function get<U>(url: string): Promise<U> {
    return getUrl<u>(url);
}

但是,可以这样调用它(U 被 TS 设置为 any):

get('/user-url');

有没有办法定义这个函数,这样它就需要明确地提供 U,如

get<User>('/user-url');

【问题讨论】:

  • 您对 getUrl 的 TypeScript 定义是什么?通过添加该要求,您将实现什么目标?
  • @jaboja 我认为用例很清楚,我在这里看到了很多关于为什么get(任何API)返回{}any而不是什么的问题我希望它,或者我如何知道我的 API 返回什么类型。 get 总是有一个人们一开始看不到的类型参数。有了这样的限制,它变得强制性和明显
  • 您要解决什么问题?毕竟,U 将默认为 {},当您尝试访问其上的任何属性时,您会收到错误消息,这会提醒您注意问题。

标签: javascript typescript


【解决方案1】:

没有对此的内置支持,但是我们可以设计一个场景,使用默认的泛型类型参数和条件类型,不传入类型参数将产生错误。即我们会给U一个默认值void。如果默认值是U的实际值,那么我们将把参数作为不应该真正传入的东西输入到函数中,从而得到错误:

async function get<U = void>(url: string & (U extends void ? "You must provide a type parameter" : string)): Promise<U> {
    return null as any;
}

get('/user-url'); // Error Argument of type '"/user-url"' is not assignable to parameter of type '"You must provide a type parameter"'.

class User {}
get<User>('/user-url');

错误信息并不理想,但我认为它会传达信息。

编辑:有关在参数类型中使用类型参数的解决方案,请参阅here

【讨论】:

  • 这样做将不允许您实际使用extends 来设置泛型类型约束
  • 感谢您提供其他答案的链接。 get('/user-url') 不会进行类型检查,但 const n: number = await get('/user-url'); 会。您需要其他答案中的NoInfer 拳击技巧来解决这个问题。
【解决方案2】:

你可以使用多个类型参数:

function contractType<T = void, U extends T = T>(value: unknown): U {
    return value as U
}

const example1: string = contractType(17) // error
const example2: string = contractType("value") // error
const example3: string = contractType<string>("value") // ok

https://github.com/Microsoft/TypeScript/issues/14829#issuecomment-288902999

【讨论】:

    【解决方案3】:
    function createCommand<P extends WhatEver = never>(a: P): P {
        a.whatEverProperty; // a is always instanceof WhatEver. Never is overlooked here.
    }
    

    大多数情况下最简单的解决方案是将never 指定为默认类型参数值。 never 始终可以分配给类型参数,即使您使用 WhatEver 扩展它,所以函数体不受影响,但这里的返回值将是 never 影响消费者以任何有意义的方式使用此函数的能力.

    (消费者/调用者在尝试使用返回值时会收到错误,但如果您的函数纯粹是为了副作用而调用,您可能需要考虑@Titian 的回答中概述的方法)

    当你通常不会返回P时,你可以使用条件类型来设置返回类型never

    function createCommand<P extends WhatEver = never>(a: P): P['whatEverProperty'] {
        // a is always instanceof WhatEver. Never is overlooked here.
        return a.whatEverProperty as P extends WhatEver ?
          ? WhatEver
          : never
    }
    

    【讨论】:

    • 这并没有解决上下文推理的问题。 const s: string = get("./abc") 将推断出泛型类型 U = string。实际问题是用户没有使用泛型参数来键入函数参数。它们实际上应该返回 unknown 并在运行时显式地进行类型检查或强制转换。
    【解决方案4】:

    另一种方法!需要一个仅用于类型推断的参数:

    /**
     * getFoo gives you a well-typed Foo component
     * ```tsx
     * const foo = getFoo(config)
     * ```
     */
    const getFoo = <Keys extends string>(
      _config: Record<Keys, unknown>
    ) => {
      return FooUntyped as FooType<Keys>
    }
    

    如果您有现成的对象可用于此类型推断,并且您的函数不会接受参数,那么这对您来说可能是一个好方法*从 TS v4.1.2 开始

    如果你没有现成的配置对象,调用它有点“wtf”,但你直接在调用它的地方得到一个错误,而不是在更远的地方:

    const foo = getFoo({} as Record<'my' | 'keys', unknown>)
    

    这种设计/方法碰巧实际上修复了我在另一个位置的设计中的一个小问题。实际上,我还添加了一个额外的参数,我的 getFoo 称为 defaultProps,这是给定 Foo 组件的一些默认道具(Foo 是表单字段组件,配置键是有效的字段名称)

    【讨论】:

    • 有点切线,但在这种情况下,1 个钩子返回 1 个组件实际上似乎是一个公平的模式。 (然而,返回多个组件的钩子会妨碍摇树)
    【解决方案5】:

    这里的其他答案提供了一些体面的解决方法,让您的代码按照您想要的方式工作,但它们都没有解决根本问题,即您试图进行不安全的强制转换以使类型检查器满意。

    我没有你的 getUrl&lt;T&gt; 函数的来源,但我强烈怀疑它看起来很像

    function getUrl<T>(url: string): Promise<T> {
      return fetch(url).then(resp => resp.json());
    }
    

    在这种情况下,Response#json() 返回Promise&lt;any&gt;,您正在悄悄地将其转换为Promise&lt;T&gt;——这是不安全的。当您编写const user: User = await get("./user"); 时,类型检查器很乐意推断您想要返回Promise&lt;User&gt; 并为您填写。即使您提供默认值,它也会这样做。

    更安全的方法是让getUrl 返回unknown。这将迫使您在运行时使用类型保护检查您的返回值,或者至少显式转换它((await get("./user")) as User 等),在这种情况下,您的 linter 会因不安全的转换而责备您。

    【讨论】:

      猜你喜欢
      • 2021-12-08
      • 2020-11-11
      • 2021-02-05
      • 2021-12-31
      • 2023-04-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多