首页 > 解决方案 > 通过 TypeScript 中的另一个泛型类型保留泛型类型

问题描述

我正在考虑在@testing-library/react和/或中提交类型声明的补丁(或补丁) @testing-library/dom。目前,所有关于RenderResult返回变体的查询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's 方法。

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

@testing-library/dom

类型/查询.d.ts

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

export const getByText: GetByText

类型/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

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

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

除了我在上面所做的更改(使每个查询的类型通用)之外,我还更改Queryget-queries-for-element.d.ts

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

我几乎可以肯定问题出在BoundFunctionorgetQueriesForElement()中,因为当我进行这些更改时(我有 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在做什么。我需要做什么才能使 maketypeof queries满足Queries约束?

TS游乐场

标签: typescriptreact-testing-library

解决方案


我很确定这是 TypeScript 的设计限制,尽管我在 GitHub 中没有找到适用范围狭窄的问题。一般的问题是,TypeScript 用来检查两种类型之间可分配性的启发式方法没有执行真正的统一。有关在 TypeScript 中实现这种统一的可能性的讨论,请参阅microsoft/TypeScript#30134 。

关于您面临的问题,我能想到的最短示例是:

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,该参数被限制为数字文字 type 0,不接受函数参数,并返回 type 的值T。该函数widerGenFn()完全相同,只是它返回一个 type 的值T | 1

如果有人在你背后替换,你应该不会注意到widerGenFngenFn如果您调用widerGenFn<ABC>(),您将期望返回一个类型的值,并且返回一个类型的值ABC | 1这一事实不会打扰您,因为每个类型的值也是一个类型的值。这意味着 of 的类型应该可以分配给的类型。genFnABCABCABC | 1genFn 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中要求的所谓的“高级类型” 。它似乎立即用特定类型替换T目标类型。因此,对于genFn,编译器会更改<T extends 0>() => T并仅替换T为其约束, 0。SogenFn被视为类型() => 0,不可分配给<T extends 0>() => T | 1.

并且有一个错误。

这意味着您在尝试使用通用参数默认值时也会出错,因为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!
// ---------------------------> ~~~~~

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

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

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

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

Playground 代码链接


推荐阅读