在TypeScript中使用条件类型的复杂函数。

huangapple go评论80阅读模式
英文:

Complex function with conditional types in Typescript

问题

我正在尝试为一个接受方法名称列表、原始对象(可能包含或不包含列表中的方法)以及备用对象(也可能包含或不包含列表中的方法)的函数添加类型。该函数遵循以下算法,针对方法列表中的每个方法执行以下操作:

  1. 如果原始对象中存在该方法,则将其添加到结果对象中。
  2. 如果备用对象中存在该方法,则将其添加到结果对象中。
  3. 如果以上两者都不存在,则将空方法(空函数)添加到结果对象中。

我编写了以下代码,但它在代码和返回值方面都存在错误:

type Reflection<
  PropertyName extends PropertyKey,
  ReflectingObject extends Partial<Record<PropertyName, Function>>,
  FallbackObject extends Record<PropertyName, Function>
> = {
  [Prop in PropertyName]: ReflectingObject[Prop] extends never
    ? FallbackObject[Prop] extends never
      ? () => void
      : FallbackObject[Prop]
    : ReflectingObject[Prop];
  };

const noop = () => { }

const reflectMethods = <
  PropertyName extends PropertyKey,
  ReflectingObject extends Partial<Record<PropertyName, Function>>,
  FallbackObject extends Record<PropertyName, Function>
>(
  reflectingObject: Partial<ReflectingObject>,
  methodNames: PropertyName[],
  fallbacks: Partial<FallbackObject> = {},
): Reflection<PropertyName, ReflectingObject, FallbackObject> =>
  methodNames.reduce<
    Reflection<PropertyName, ReflectingObject, FallbackObject>
  >((reflection, name) => {
    reflection[name] = reflectingObject[name] ?? fallbacks[name] ?? noop; // 此处代码会出现 T2322 错误

    return reflection;
  }, {} as Reflection<PropertyName, ReflectingObject, FallbackObject>);

另外,如果我使用这个函数,如果我没有提及原始对象或备用对象中的任何方法,它将失败,尽管它应该具有() => void类型。

class A {
    a(aa: number) { 
        return aa;
    }
    b(bb: string) { 
        return bb;
    }
    c() {}
 }

const proto = A.prototype;

const aaaa = reflectMethods(proto, ['a', 'c', 'd'], { a(aa: number) { return aa + 2 } });

aaaa.a(11);
aaaa.d(); // 此处会出现 T2571 错误

我可能在这里某处错误地使用了条件类型,但我不确定如何修复它们以使其正常工作。以下是演示此问题的 TypeScript Playground 链接

英文:

I'm trying to add types to a function that takes a list of method names, the original object that may contain or not contain a method from the list, and the fallback object that also can include or not include a method from the list.

The function follows the algorithm for each method in the method list:

  1. If the method exists in the original object, it is added to the result object.
  2. If a method exists in the fallback object, it is added to the result object.
  3. If nothing above exists, the no-op method (empty function) is added to the result object.

I wrote the following code, but it has errors in both code and returns value:

type Reflection<
  PropertyName extends PropertyKey,
  ReflectingObject extends Partial<Record<PropertyName, Function>>,
  FallbackObject extends Record<PropertyName, Function>
> = {
  [Prop in PropertyName]: ReflectingObject[Prop] extends never
    ? FallbackObject[Prop] extends never
      ? () => void
      : FallbackObject[Prop]
    : ReflectingObject[Prop];
  };

const noop = () => { }

const reflectMethods = <
  PropertyName extends PropertyKey,
  ReflectingObject extends Partial<Record<PropertyName, Function>>,
  FallbackObject extends Record<PropertyName, Function>
>(
  reflectingObject: Partial<ReflectingObject>,
  methodNames: PropertyName[],
  fallbacks: Partial<FallbackObject> = {},
): Reflection<PropertyName, ReflectingObject, FallbackObject> =>
  methodNames.reduce<
    Reflection<PropertyName, ReflectingObject, FallbackObject>
  >((reflection, name) => {
    reflection[name] = reflectingObject[name] ?? fallbacks[name] ?? noop; // here the code fails with T2322

    return reflection;
  }, {} as Reflection<PropertyName, ReflectingObject, FallbackObject>);

Also, if I use this function, it will fail if I do not mention any of the methods in the original object or the fallback object though it should have the () => void type.

class A {
    a(aa: number) { 
        return aa;
    }
    b(bb: string) { 
        return bb;
    }
    c() {}
 }

const proto = A.prototype;

const aaaa = reflectMethods(proto, ['a', 'c', 'd'], { a(aa: number) { return aa + 2 } });

aaaa.a(11);
aaaa.d(); // here it fails with T2571

I probably misused the conditional types somehow here, but I'm not sure how to fix them to make them work.

Here is the link to the Typescript playground that demonstrates the issue.

答案1

得分: 1

以下是我试图理解示例后得到的代码:

const noop = () => { }
type Noop = typeof noop;

type Reflection<
  K extends PropertyKey, R, F> = {
    [P in K]: P extends keyof R ? R[P] : P extends keyof F ? F[P] : Noop;
  };

type Funs<K extends PropertyKey> = { [P in K]?: Function }

const reflectMethods = <K extends PropertyKey, R, F>(
  reflectingObject: R & Funs<K>,
  methodNames: K[],
  fallbacks?: F & Funs<K>,
) =>
  methodNames.reduce((reflection, name) => {
    reflection[name] = reflectingObject[name] ?? fallbacks?.[name] ?? noop;
    return reflection;
  }, {} as Funs<K>) as Reflection<K, R, F>;

不太重要的更改:

  • 将类型参数更改为单字符名称,按照约定。PropertyName 变为 KReflectingObject 变为 RFallbackObject 变为 FProp 变为 P

  • 添加类型别名 Noop 用于 () => voidFuns<K> 用于 Partial<Record<K, Function>>

较重要的更改:

  • 不再将 FR 限制为 Funs<K>,而是只需确保 reflectMethods() 函数保证 methodNamesfallbacks 可以分配给 Funs<K>,同时将它们推断为传入的狭窄类型。泛型约束有时会产生不希望的行为,使类型参数扩展到约束,这完全违背了您的目的。您不希望 RF 具有来自 K 的所有键,除非它们实际上在所有这些属性上都具有函数。根据值推断类型参数 T 或甚至 T & SomethingElse 是相当简单的,而根据值推断类型参数 T,给定 Partial<T> 的值则不太可靠。

  • 不再使用条件类型来探测 F[P] extends never,而是使用它来探测 P extends keyof F。如果不将 F 限制为 Funs<K>,这是必要的,而且更常规 (K extends keyof T ? T[K] : SomethingElse 非常常见,T[K] extends never ? SomethingElse : T[K] 不太常见)。

  • 我将 fallbacks 更改为可选参数,并在稍后,如果 fallbacksundefined,则使用 fallbacks?.[name]。这与将 fallbacks 默认为 {} 的结果相同,但在类型系统中更加友好。

  • 分配一个依赖于未指定泛型的条件类型的值通常是不可能的,除非使用类型断言;编译器在这些类型上没有进行太多分析。Reflection<K, R, F> 就是在 reflectMethods() 的实现中这种类型。我不再尝试在 reflection[name] 处分配一个值,其中 reflectionReflection<K, R, F>,而是在值返回时将 reflection 设置为 Funs,然后进行断言为 Reflection<K, R, F>

好的,让我们看看结果如何:

const aaaa = reflectMethods(proto, ['a', 'c', 'd'], { a(aa: number) { return aa + 2 } });
aaaa.a // (aa: number) => number;
aaaa.c // () => void
aaaa.d // () => void

aaaa.a(11); // 可以
aaaa.d(); // 可以

这一切看起来对我来说都很好。您可能希望进一步探究边缘情况,但希望这为您提供了一些方向。祝好运!

英文:

Here's the code I ended up with after trying to rationalize what was happening in the example:

const noop = () =&gt; { }
type Noop = typeof noop;
type Reflection&lt;
K extends PropertyKey, R, F&gt; = {
[P in K]: P extends keyof R ? R[P] : P extends keyof F ? F[P] : Noop;
};
type Funs&lt;K extends PropertyKey&gt; = { [P in K]?: Function }
const reflectMethods = &lt;K extends PropertyKey, R, F&gt;(
reflectingObject: R &amp; Funs&lt;K&gt;,
methodNames: K[],
fallbacks?: F &amp; Funs&lt;K&gt;,
) =&gt;
methodNames.reduce((reflection, name) =&gt; {
reflection[name] = reflectingObject[name] ?? fallbacks?.[name] ?? noop;
return reflection;
}, {} as Funs&lt;K&gt;) as Reflection&lt;K, R, F&gt;;

Unimportant changes:

  • making type parameters single-character names, as per convention. PropertyName became K, ReflectingObject became R, FallbackObject became F, and Prop became P.

  • adding type aliases Noop for ()=&gt;void and Funs&lt;K&gt; for Partial&lt;Record&lt;K, Function&gt;&gt;.

Important-ish changes:

  • Instead of having F and R constrained to Funs&lt;K&gt;, I just have the reflectMethods() function guarantee that methodNames and fallbacks are assignable to Funs&lt;K&gt; while inferring them as the narrow type that's handed in. Generic constraints sometimes have undesirable behavior of making the type parameter widen all the way to the constraint, which completely defeats your purpose. You don't want R and F to have all the keys from K unless they actually have functions at all those properties. It's also fairly straightfoeward to have the compiler infer a type parameter T given a value of T or even T &amp; SomethingElse, whereas it's less reliable to infer a type parameter T given a value of Partial&lt;T&gt;.

  • Instead of having the conditional type probe for F[P] extends never, I have it probe P extends keyof F. This is necessary if you don't constrain F to Funs&lt;K&gt;, and it's more conventional too (K extends keyof T ? T[K] : SomethingElse is very common: T[K] extends never ? SomethingElse : T[K] is much less common).

  • I've made fallBacks an optional parameter and later, if fallBacks is undefined, I use fallbacks?.[name]. This gives the same results as if you made fallBacks default to {}, but plays more nicely with the type system.

  • Assigning a value to a conditional type that depends on an unspecified generic is often impossible without a type assertion; the compiler just doesn't do much analysis on these types. And Reflection&lt;K, R, F&gt; is such a type inside the implementation of reflectMethods(). Instead of trying to assign reflection[name] where reflection is a Reflection&lt;K, R, F&gt;, I instead let reflection be a Funs and then assert to Reflection&lt;K, R, F&gt; when the value is returned.


Okay, let's see what comes out:

const aaaa = reflectMethods(proto, [&#39;a&#39;, &#39;c&#39;, &#39;d&#39;], { a(aa: number) { return aa + 2 } });
aaaa.a // (aa: number) =&gt; number;
aaaa.c // () =&gt; void
aaaa.d // () =&gt; void
aaaa.a(11); // okay
aaaa.d(); // okay

That all looks good to me. You might want to do more probing of edge cases, but hopefully that gives you some direction. Good luck!

Playground Link

huangapple
  • 本文由 发表于 2020年1月4日 01:57:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/59583176.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定