打字稿:传递给泛型类型的参数类型未按预期解析

时间:2021-02-10 15:44:08

标签: typescript

骨干演示

const DirectedPropertySets = {
    start: ["left", "top"] as const, // for DOMRectReadOnly
    client: ["clientX", "clientY"] as const // for PointerEvent
};

type DirectedPropertySet<T> = {
    [P in keyof typeof DirectedPropertySets]: typeof DirectedPropertySets[P][0] extends keyof T ? P : never;
}[keyof typeof DirectedPropertySets];

/**
 * One of usage of this whole thing to show you why I'm making things like this
 * Don't be bothered by this
 */
function getFromRect(rect: DOMRectReadOnly, key: DirectedPropertySet<DOMRectReadOnly>) {
    const set = DirectedPropertySets[key];
    /**
     * const set: readonly ["left", "top"]
     */
    return rect[set[0]]; // fine
}

/**
 * This is the issue that produces an error
 */
function getFromEvent<T extends Event>(event: T, key: DirectedPropertySet<T>) {
    const set = DirectedPropertySets[key];
    /**
     * const set: {
     *      start: readonly ["left", "top"];
     *      client: readonly ["clientX", "clientY"];
     * }[DirectedPropertySet<T>]
     */
    return event[set[0]]; // error
}

/**
 * The previous one that has a change not to produce an error
 */
function getFromEvent<T extends Event>(event: T, key: DirectedPropertySet<Event>) {
    const set = DirectedPropertySets[key];
    /**
     * const set: never
     * it's okay to have type of never as I intended it
     */
    return event[set[0]]; // fine
}

从局部变量set的类型如何根据参数key的类型而变化,我认为DirectedPropertySet<T>中的key: DirectedPropertySet<T>只是指的定义类型 DirectedPropertySet<T> 而不是实际将泛型类型 T extends Event 应用到 DirectedPropertySet<T> 中。

我对 Typescript 的泛型类型有什么误解?

根据@jcalz 的要求,我将尽可能简单地展示我对它的实际使用。您会看到 getFromEvent 中实际上还有另一个参数。

// "pointermove" event listener
function onPointerMove(ev: PointerEvent) {
    /**
     * Only "client" is visible,
     * which means DirectedPropertySet<T> of getFromEvent is resolved,
     * but then why the DirectedPropertySets[key][axis]
     * can't index "event" parameter of getFromEvent?
     */
    const ... = getFromEvent(ev, "client", 0);
    ...
}

// parameter "axis" is added to index DirectedPropertySets[key]
function getFromEvent<T extends Event>(event: T, key: DirectedPropertySet<T>, axis: number) {
    const property = DirectedPropertySets[key][axis];
    // return event[property]; // produces an error
    return event[property as Extract<typeof property, keyof T>];
}

Typescript 的首席开发人员 answered 关于这个问题。

(注意我给了他一个不同的例子,所以类型名称和类型声明有点不同。)

<块引用>

TS 没有办法意识到 Intersecting 中唯一能产生的东西是 keyof Ts

我理解为 TS 无法解析一些复杂的参数类型。

1 个答案:

答案 0 :(得分:1)

发生这种情况的一般原因与编译器是否延迟评估依赖于尚未指定的泛型参数(如 T getFromEvent()) 的实现,或者它是否急切评估它。我真的不知道编译器用来做出决定的启发式方法的细节,但我将简要提及两者的优缺点以及为什么这是一个难题。有关详细信息,请参阅 this comment on microsoft/TypeScript#33181


当编译器推迟对类型的评估时,一旦指定了泛型类型参数,您最终将获得相当精确的类型。那挺好的。但是在这发生之前,编译器通常不知道是否有任何给定的值可以分配给它。因此,如果您尝试生成/返回此类类型的值,编译器很可能会产生警告,即使您知道它是安全的。那很糟。例如:

type KeysMatching<T, V> = NonNullable<
  { [K in keyof T]: T[K] extends V ? K : never }[keyof T]
>;
interface Foo {
  a: string,
  b: number,
  c: boolean
};
type StringFoo = KeysMatching<Foo, string> // "a"

此处,KeysMatching<T, V> 将采用对象类型 T 并返回其属性可分配给 V 的任何键。所以 StringFoo 就是 "a"。但是在 T 是泛型的泛型函数中,编译器无法遵循它:

function foo<T>(t: T, k: KeysMatching<T, string>): string {
  return t[k]; // error! 
  // 'T[NonNullable<{ [K in keyof T]: T[K] extends string ? K : never; }[keyof T]>]'
  //  is not assignable to type 'string'
}

在这里,类型被延迟:虽然事实证明 T[KeysMatching<T, string>] 应该可以分配给 string,但这将是编译器无法执行的高阶类型分析。所以你会得到一个错误。如果你想解决这个问题,你可能需要一个 type assertion:

return t[k] as any as string; // ?‍♂️

当编译器急切评估一个未指定的泛型类型时,它本质上放弃了正确性,只是为泛型类型插入了泛型约束。因此,如果 T extends Foo,则 T 被替换为 Foo。这并不总是正确的,即使它是正确的,它通常也没有它应有的精确。但它很方便,并且编译器允许您为其分配内容……即使这样做可能不安全。举个例子:

function bar<T extends Foo>(foo: T) {
    foo.b = 1; // uh oh, foo.b is eagerly evaluated as number
}

interface ZeroB extends Foo { b: 0 };
const zeroB: ZeroB = { a: "", b: 0, c: true };
bar(zeroB); // oops!  zeroB.b is now 1 at runtime but the compiler thinks it's zero

bar() 中,属性类型 foo.b 被编译器急切地评估为 Foo['b'],即 number,即使它在技术上应该是 {{1} }.如果您有一个类似 T['b'] 的类型匹配 ZeroB 但有一个 Foo 属性,该属性只能是数字文字 b,并将该类型的值传递给 {{ 1}},坏事发生了。允许将 0 分配给 bar() 很方便,但不安全。


无论如何,对于您的示例代码,问题似乎是急切地评估了 number 的类型。类型 foo.b 被替换为 DirectedPropertySets[key][axis](如果约束是逆变的,则可能是 T),因此 Event 被视为类似于 never。这不是 DirectedPropertySets[key][axis] 的键,所以有错误。

理想情况下,您希望编译器延迟类型,尽管您可能必须说服它访问是安全的。为此,我将添加更多泛型类型参数以帮助延迟,并添加类型断言以防止错误:

"left" | "top" | "clientX" | "clientY"

这里所有三个函数输入都有自己的类型参数,因此 T 被适当地推迟为类似于 function getFromEvent<T extends Event, K extends DirectedPropertySet<T>, A extends number>( event: T, key: K, axis: A) { const property = DirectedPropertySets[key][axis]; /* { start: readonly ["left", "top"]; client: readonly ["clientX", "clientY"]; }[K][A] */ return event[property as Extract<keyof T, typeof property>]; } 的东西。这仍然不能理解为可分配给 property,因此我使用断言告诉编译器它可以将 (typeof DirectedPropertySets)[K][A] 视为 keyof T 和延迟 property(使用Extract<T, U> utility type)。

现在一切都按预期进行,我想。函数的返回类型比较笨重

keyof T

但是如果您为 typeof propertyT[Extract<keyof T, { start: readonly ["left", "top"]; client: readonly ["clientX", "clientY"]; }[K][A]>] T 插入特定类型,它会起作用:

K

请注意,上面的断言可能有点不正确……如果 A 的类型像 function onPointerMove(ev: PointerEvent) { const d = getFromEvent(ev, "client", 0); // number } 这样疯狂,那么会发生这种情况:

T

编译器发现 Event & {clientX: Date} 没有 function hmm(ev: Event & { clientX: Date }) { const d = getFromEvent(ev, "client", 1); // never! } 属性并从 ev 返回 clientY。这不完全正确。它可能是 never。可能有解决这个的方法,但答案已经很长了,我不想让它更长。可以说在处理泛型函数的实现时应该谨慎行事。

Playground link to code