[英]Conditional and Existential Types on Translated Typescript Object
I hope this makes sense.我希望这是有道理的。
I'm trying to write a function that takes an object where each key equals a function and returns a function who's rest argument(s) are a translation of that object to [a key of the object, and the params of that key's function] I'm trying to write a function that takes an object where each key equals a function and returns a function who's rest argument(s) are a translation of that object to [a key of the object, and the params of that key's function]
export const useConfig =
<T extends Record<string, (...a: any) => null>>(features: T) =>
<O extends T, K extends keyof O>(...args: [K, ...Parameters<O[K]>][]): void =>
void 0;
const config = useConfig({
attr: (prop: 'id', val: string) => null,
style: (prop: 'font', val: 'arial') => null,
});
This const t2 = config(['attr', 'font','']);
这个
const t2 = config(['attr', 'font','']);
shouldn't work, it should:不应该工作,它应该:
Any help would be much appreciated.任何帮助将非常感激。 I feel like I'm just not getting something fundamental about the type system.
我觉得我只是没有得到关于类型系统的一些基本知识。
In what follows I'll call the operation in question, turning a key K
(which extends keyof T
for some suitable T
whose values are all function types) into the tuple [K, ...Parameters<T[K]>]
, "parameterizing" K
, or a "parameterization" of K
.在下文中,我将调用有问题的操作,将键
K
(它extends keyof T
为一些合适的T
,其值都是 function 类型)进入元组[K, ...Parameters<T[K]>]
, “参数化” K
,或K
的“参数化”。
Conceptually you want the return type of useConfig
to be a function which accepts a variadic number of arguments, where each argument is a parameterization of some key in keyof T
.从概念上讲,您希望 useConfig 的返回类型为
useConfig
,它接受可变数字 arguments,其中每个参数都是keyof T
中某个键的参数化。 You don't really want to know which argument is a parameterization of which key, just that each one corresponds to some key.您真的不想知道哪个参数是哪个键的参数化,只是每个参数对应于某个键。 This use of "some" is indeed a hint that the kind of generic quantification you'd need here is existential instead of the "normal" universal quantification.
这种“一些”的使用确实暗示你在这里需要的那种通用量化是存在的而不是“正常”的通用量化。 You can think of normal, universal generics as intersections over every acceptable type, while existential generics are unions over every acceptable type.
您可以将普通的通用 generics 视为每个可接受类型的交集,而存在的generics是每个可接受类型的并集。
And here, since "every acceptable type" is just the single members of keyof T
, then you can represent this union directly.在这里,由于“每个可接受的类型”只是
keyof T
的单个成员,因此您可以直接表示这个联合。 All you want to do is distribute the parameterization operation over the union in K
to make a new union.您要做的就是将参数化操作分布在
K
中的并集上以创建一个新的并集。
If you want to distribute an operation over keylike types, you can use a distributive object type (as coined in microsoft/TypeScript#47109 ) where you make a mapped type and then immediately index into it .如果你想在 keylike 类型上分发一个操作,你可以使用一个分发的 object 类型(在microsoft/TypeScript#47109中创造),你可以在其中创建一个映射类型,然后立即对其进行索引。 If you have a key set
KS
and you want to distribute the operation F<K>
over it, you can write that like {[K in KS]: F<K>}[KS]
.如果你有一个键集
KS
并且你想在它上面分配操作F<K>
,你可以这样写{[K in KS]: F<K>}[KS]
。 In your case KS
is keyof T
and F<K>
is [K, ...Parameters<T[K]>]
.在您的情况下,
KS
是keyof T
而F<K>
是[K, ...Parameters<T[K]>]
。 So you get this:所以你得到这个:
const useConfig =
<T extends Record<string, (...a: any) => null>>(features: T) =>
(...args: { [K in keyof T]-?: [K, ...Parameters<T[K]>] }[keyof T][]): void =>
void 0;
Let's see what happens when we call it:让我们看看当我们调用它时会发生什么:
const config = useConfig({
attr: (prop: 'id', val: string) => null,
style: (prop: 'font', val: 'arial') => null,
});
/* const config: (...args: (
["attr", "id", string] | ["style", "font", "arial"]
)[]) => void */
So this is exactly what you want.所以这正是你想要的。 Each argument passed into
config()
should be a tuple of type ["attr", "id", string]
or one of type ["style", "font", "arial"]
.传递给
config()
的每个参数应该是["attr", "id", string]
类型或["style", "font", "arial"]
类型之一的元组。 And you'll get the type checking you care about:你会得到你关心的类型检查:
const t = config(
["attr", "id", "abc"], // okay
["style", 'font', "arial"], // okay
["attr", "font", ""] // error!
//~~~~~~~~~~~~~~~~~~
);
Unfortunately that doesn't give you a great experience with IntelliSense and autocompletion.遗憾的是,这并不能为您提供出色的 IntelliSense 和自动完成体验。 This isn't really a problem with the above solution, but a limitation or missing feature of TypeScript, which is requested at microsoft/TypeScript#38603 .
这并不是上述解决方案的真正问题,而是 TypeScript 的限制或缺失功能,这是在microsoft/TypeScript#38603中请求的。
We can work around this by having the compiler actually try to figure out which argument is a parameterization of which key.我们可以通过让编译器实际尝试找出哪个参数是哪个键的参数化来解决这个问题。 That means if we call
这意味着如果我们打电话
const t = config(
["attr", "id", "abc"],
["style", 'font', "arial"],
["attr", "font", "abc"]
);
the compiler needs to know "the first key is "attr"
, the second one is "style"
, and third one is "attr"
. If you think of this as a tuple of keys KS
, then here KS
is ["attr", "style", "attr"]
. And we need to represent the operation of turning the KS
tuple into a tuple of parameterizations of each key in the tuple. That is, we want to map the parameterization operation over the input tuple to get an output tuple.编译器需要知道“第一个键是
"attr"
,第二个是"style"
,第三个是"attr"
。如果你认为这是键KS
的元组,那么这里KS
是["attr", "style", "attr"]
。而我们需要表示将KS
元组转化为元组中每个键的参数化元组的操作。即我们要map对输入元组进行参数化操作得到一个 output 元组。
That version looks like this:该版本看起来像这样:
const useConfig =
<T extends Record<string, (...a: any) => null>>(features: T) =>
<KS extends Array<keyof T>>(...args: {
[I in keyof KS]: [KS[I], ...Parameters<T[Extract<KS[I], keyof T>]>]
}): void =>
void 0;
It's a bit more involved;这有点复杂; the compiler does map tuples to tuples with
{[I in keyof KS]: F<I>}
acting only on the numeric-like indices I
, but it doesn't quite know that it's doing this inside the body of the mapped type, so it will balk if you treat KS[I]
as if it's keyof T
, (because "wHaT iF I
iS somE arrAy meTHod nAme LIke "push"
? see ms/TS#27995 ). We need to use the Extract<T, U>
utility type to convince the compiler that KS[I]
can be treated as if it's assignable to keyof T
.编译器将 map 元组转换为元组,其中
{[I in keyof KS]: F<I>}
仅作用于类似数字的索引I
,但它不太清楚它是在映射类型的主体内执行此操作,因此,如果您将KS[I]
视为keyof T
,它会犹豫不决(因为“如果I
是某种类似于"push"
的方法名称?请参阅ms/TS#27995 )。我们需要使用Extract<T, U>
实用程序类型以说服编译器KS[I]
可以被视为可分配给keyof T
。
Now when we call config()
:现在当我们调用
config()
时:
const t = config(
["attr", "id", "abc"], // okay
["style", 'font', "arial"], // okay
["attr", "font", "abc"], error!
// -----> ~~~~ <---- error here
);
/* const config: <["attr", "style", "attr"]>(
args_0: ["attr", "id", string],
args_1: ["style", "font", "arial"],
args_2: ["attr", "id", string]) => void
*/
You can see that the compiler infers ["attr", "style", "attr"]
for KS
, and then is unhappy specifically about the invalid "font"
value in the args_2
argument.您可以看到编译器为
KS
推断["attr", "style", "attr"]
,然后特别不满args_2
参数中的无效"font"
值。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.