英文:
Complex function with conditional types in Typescript
问题
我正在尝试为一个接受方法名称列表、原始对象(可能包含或不包含列表中的方法)以及备用对象(也可能包含或不包含列表中的方法)的函数添加类型。该函数遵循以下算法,针对方法列表中的每个方法执行以下操作:
- 如果原始对象中存在该方法,则将其添加到结果对象中。
- 如果备用对象中存在该方法,则将其添加到结果对象中。
- 如果以上两者都不存在,则将空方法(空函数)添加到结果对象中。
我编写了以下代码,但它在代码和返回值方面都存在错误:
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:
- If the method exists in the original object, it is added to the result object.
- If a method exists in the fallback object, it is added to the result object.
- 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
变为K
,ReflectingObject
变为R
,FallbackObject
变为F
,Prop
变为P
。 -
添加类型别名
Noop
用于() => void
和Funs<K>
用于Partial<Record<K, Function>>
。
较重要的更改:
-
不再将
F
和R
限制为Funs<K>
,而是只需确保reflectMethods()
函数保证methodNames
和fallbacks
可以分配给Funs<K>
,同时将它们推断为传入的狭窄类型。泛型约束有时会产生不希望的行为,使类型参数扩展到约束,这完全违背了您的目的。您不希望R
和F
具有来自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
更改为可选参数,并在稍后,如果fallbacks
为undefined
,则使用fallbacks?.[name]
。这与将fallbacks
默认为{}
的结果相同,但在类型系统中更加友好。 -
分配一个依赖于未指定泛型的条件类型的值通常是不可能的,除非使用类型断言;编译器在这些类型上没有进行太多分析。
Reflection<K, R, F>
就是在reflectMethods()
的实现中这种类型。我不再尝试在reflection[name]
处分配一个值,其中reflection
是Reflection<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 = () => { }
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>;
Unimportant changes:
-
making type parameters single-character names, as per convention.
PropertyName
becameK
,ReflectingObject
becameR
,FallbackObject
becameF
, andProp
becameP
. -
adding type aliases
Noop
for()=>void
andFuns<K>
forPartial<Record<K, Function>>
.
Important-ish changes:
-
Instead of having
F
andR
constrained toFuns<K>
, I just have thereflectMethods()
function guarantee thatmethodNames
andfallbacks
are assignable toFuns<K>
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 wantR
andF
to have all the keys fromK
unless they actually have functions at all those properties. It's also fairly straightfoeward to have the compiler infer a type parameterT
given a value ofT
or evenT & SomethingElse
, whereas it's less reliable to infer a type parameterT
given a value ofPartial<T>
. -
Instead of having the conditional type probe for
F[P] extends never
, I have it probeP extends keyof F
. This is necessary if you don't constrainF
toFuns<K>
, 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, iffallBacks
isundefined
, I usefallbacks?.[name]
. This gives the same results as if you madefallBacks
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<K, R, F>
is such a type inside the implementation ofreflectMethods()
. Instead of trying to assignreflection[name]
wherereflection
is aReflection<K, R, F>
, I instead letreflection
be aFuns
and then assert toReflection<K, R, F>
when the value is returned.
Okay, let's see what comes out:
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); // 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!
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论