通过 TypeScript 中的另一个泛型类型保留泛型类型

Preserving generic types through another generic type in TypeScript

我正在考虑提交 @testing-library/react and/or @testing-library/dom 中类型声明的一个(或多个)补丁。目前,所有关于 RenderResult return 变体的查询 HTMLElement,这让我写了很多代码,比如

const myDiv = wrapper.getByText('some text') as HTMLDivElement;

最简单的解决方案是使查询通用。

export type GetByText = <T extends HTMLElement = HTMLElement>(/* ... */) => T
export const getByText: GetByText

那我可以这样做:

const myDiv = wrapper.getByText<HTMLDivElement>('some text');

虽然这在 TS Playground 中有效,但 @testing-library/react 中的某些内容阻止将通用形式传递给 RenderResult 的方法。

这些是 @testing-library/react@testing-library/dom 当前版本的相关片段。

@testing-library/dom

types/queries.d.ts

export type GetByText = (
  container: HTMLElement,
  id: Matcher,
  options?: SelectorMatcherOptions,
) => HTMLElement

export const getByText: GetByText

types/get-queries-for-element.d.ts

import * as queries from './queries';

export type BoundFunction<T> = T extends (
    attribute: string,
    element: HTMLElement,
    text: infer P,
    options: infer Q,
) => infer R
    ? (text: P, options?: Q) => R
    : T extends (a1: any, text: infer P, options: infer Q, waitForElementOptions: infer W) => infer R
    ? (text: P, options?: Q, waitForElementOptions?: W) => R
    : T extends (a1: any, text: infer P, options: infer Q) => infer R
    ? (text: P, options?: Q) => R
    : never;
export type BoundFunctions<T> = { [P in keyof T]: BoundFunction<T[P]> };

export type Query = (
    container: HTMLElement,
    ...args: any[]
) => Error | Promise<HTMLElement[]> | Promise<HTMLElement> | HTMLElement[] | HTMLElement | null;

export interface Queries {
    [T: string]: Query;
}

export function getQueriesForElement<T extends Queries = typeof queries>(
    element: HTMLElement,
    queriesToBind?: T,
): BoundFunctions<T>;

@testing-library/react

(可能相关也可能不相关)
types/index.d.ts

export type RenderResult<
  Q extends Queries = typeof queries,
  Container extends Element | DocumentFragment = HTMLElement
> = {
  // ...
} & {[P in keyof Q]: BoundFunction<Q[P]>}

除了我在上面所做的更改之外,使每个查询的类型通用,我在 get-queries-for-element.d.ts 中更改了 Query:

export type Query = <T extends HTMLElement>(
    container: HTMLElement,
    ...args: any[]
) => Error | Promise<T[]> | Promise<T> | T[] | T | null;

我几乎可以肯定问题出在 BoundFunctiongetQueriesForElement(),因为当我进行这些更改时(我有 37% 的信心是正确的),我得到一个错误:

Type 'typeof import(".../node_modules/@testing-library/dom/types/queries")' does not satisfy the constraint 'Queries'.
  Property 'getByLabelText' is incompatible with index signature.
    Type 'GetByText' is not assignable to type 'Query'.
      Type 'HTMLElement' is not assignable to type 'T | Error | T[] | Promise<T[]> | Promise<T>'.
        Type 'HTMLElement' is not assignable to type 'T'.
          'HTMLElement' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'HTMLElement'. ts(2344)

@testing-library/react(以及其他测试库包)中可能存在另一个问题,但解决这个问题是第一步。

我的部分问题是我实际上并不了解 BoundFunction 在做什么。我需要做什么才能使 make typeof queries 满足 Queries 约束条件?

TS Playground

我很确定这是 TypeScript 的设计限制,尽管我在 GitHub 中没有发现适用范围较窄的问题。一般问题是 TypeScript 用于检查两种类型之间的可分配性的启发式算法不执行真实 unification. See microsoft/TypeScript#30134 讨论在 TypeScript 中实现这种统一的可能性。

我能想出的关于你面临的问题的最短的例子是:

declare const genFn: <T extends 0>() => T;
const widerGenFn: <T extends 0>() => T | 1 = genFn; // error!
// Type '<T extends 0>() => T' is not assignable to type '<T extends 0>() => T | 1'.
// Type '0' is not assignable to type 'T | 1'

函数 genFn() 有一个类型参数 T,它被限制为数字文字类型 0,没有函数参数,returns 类型的值T。函数 widerGenFn() 完全相同,除了它 returns 类型为 T | 1.

的值

如果有人在你背后把widerGenFn换成了genFn,你应该不会注意到;如果您调用 widerGenFn<ABC>(),您将期望返回一个 ABC | 1 类型的值,而 genFn returns 类型 ABC 的值这一事实不会打扰你,因为每个 ABC 类型的值也是 ABC | 1 类型的值。这意味着 genFn 的类型应该 assignable to widerGenFn.

的类型

但事实并非如此。 TypeScript 抱怨 Type '<T extends 0>() => T' is not assignable to type '<T extends 0>() => T | 1',因为 Type '0' is not assignable to type 'T | 1'。给了什么?

好吧,当像这样比较泛型函数时,编译器不会按照 microsoft/TypeScript#1213. It seems to immediately substitute a specific type for T in the target type. So for genFn, the compiler changes <T extends 0>() => T and just replaces T with its constraint0 中的要求使用所谓的“高等类型”。因此 genFn 被视为类型 () => 0,不能分配给 <T extends 0>() => T | 1.

出现错误。

这意味着您在尝试使用 generic parameter defaults 时也会出错,因为 type X<T extends U = V> 将要求 V extends U:

type GenFn = typeof genFn;
type WiderGenFn = typeof widerGenFn
type Foo<T extends WiderGenFn = GenFn> = void; // error!
// ---------------------------> ~~~~~

那么解决方法是什么?我不确定......它可能会在很大程度上取决于用例。一种方法是“扩大”对所涉及的两种类型的 union 的约束:

type Bar<T extends WiderGenFn | GenFn = GenFn> = void; // okay

(在你的情况下,这看起来像 Q extends Queries | typeof queries = typeof queries

可能还有其他方法可以避免 TypeScript 中缺乏统一性,但看到奇怪的副作用和边缘情况弹出我不会感到惊讶。

Playground link to code