通过 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;
我几乎可以肯定问题出在 BoundFunction
或 getQueriesForElement()
,因为当我进行这些更改时(我有 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
约束条件?
我很确定这是 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 constraint、0
中的要求使用所谓的“高等类型”。因此 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 中缺乏统一性,但看到奇怪的副作用和边缘情况弹出我不会感到惊讶。
我正在考虑提交 @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;
我几乎可以肯定问题出在 BoundFunction
或 getQueriesForElement()
,因为当我进行这些更改时(我有 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
约束条件?
我很确定这是 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 constraint、0
中的要求使用所谓的“高等类型”。因此 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 中缺乏统一性,但看到奇怪的副作用和边缘情况弹出我不会感到惊讶。