[英]Dynamically type an object based on an array?
type paramType = {type: string, value: string | number }
const PROFILE: Record<string, paramType> = {
color: { type: 'string', value: 'red' },
height: { type: 'number', value: 180 },
weight: { type: 'number', value: 70 },
}
const constructObj = (selectedFields: string[]) => {
return selectedFields.reduce((accum, cur) => {
return { ...accum, [cur]: PROFILE[cur] };
}, {} as Record<typeof selectedFields[number], paramType>);
}
const result = constructObj(['color']); // { color: {type: 'string', value: 'red' } }
// 1. result.color // Not throwing error, great
// 2. result.xxx // Not throwing error, Not good
constructObj
is a function that will dynamically return an object with keys defined in selectedFields
. constructObj
是一个 function ,它将动态返回一个 object ,其中键定义在selectedFields
中。 I'm facing an issue where result.xxx
is not throwing error?我面临一个result.xxx
没有抛出错误的问题?
A slightly different solution from what @ASDFGerte suggested.与@ASDFGerte 建议的解决方案略有不同。 With this you don't need the as const
when calling the array.有了这个,你在调用数组时就不需要as const
了。 Personally, think it looks cleaner.个人认为,它看起来更干净。
type paramType = { type: string; value: string | number };
const PROFILE: Record<string, paramType> = {
color: { type: "string", value: "red" },
height: { type: "number", value: 180 },
weight: { type: "number", value: 70 },
};
const constructObj = <T extends string>(
selectedFields: T[]
): Record<T, paramType> => {
return selectedFields.reduce((accum, cur) => {
return { ...accum, [cur]: PROFILE[cur] };
}, {} as Record<typeof selectedFields[number], paramType>);
};
const result = constructObj(["color"]); // { color: {type: 'string', value: 'red' } }
result.color; // Not throwing error, great
result.xxx; // Throwing error now
Addressing @Keith's comment on @nullptr's answer, we can instead use this magic to infer the complete type of selectedFields
without as const
and without the drawback that the result's value
is not string | number
针对@Keith 对@nullptr 答案的评论,我们可以改为使用这种魔法来推断selectedFields
的完整类型,而不需要as const
并且没有结果value
不是string | number
的缺点。 string | number
. string | number
。
However you do have to sacrifice a little inside the body of the function, but hey, a little sacrifice for great typings sounds great to me.但是,您确实必须在 function 的主体内部做出一些牺牲,但是,嘿,为出色的打字做出一点牺牲对我来说听起来很棒。
The core of this solution is this interesting definition:这个解决方案的核心是这个有趣的定义:
type Narrow<T> =
| (T extends infer U ? U : never)
| Extract<T, number | string | boolean | bigint | symbol | null | undefined | []>
| ([T] extends [[]] ? [] : { [K in keyof T]: Narrow<T[K]> });
Essentially it's a bunch of "tests" to get TypeScript inferring what T
is.从本质上讲,要让 TypeScript 推断出T
是什么,这是一堆“测试”。
Here is my definition of constructObj
:这是我对constructObj
的定义:
const constructObj = <T>(selectedFields: Narrow<T>): T extends string[] ? {
[K in T[number]]: K extends keyof typeof PROFILE_TYPE ? typeof PROFILE_TYPE[K] : never;
} : never => {
return (selectedFields as string[]).reduce((accum, cur) => {
return { ...accum, [cur]: PROFILE[cur] };
}, {} as ReturnType<typeof constructObj<T>>);
}
We use Narrow
to narrow down T
.我们使用Narrow
来缩小T
范围。 That means when we call it like this:这意味着当我们这样称呼它时:
constructObj(["color"]);
T
will be ["color"]
, not string[]
, Of course once we have the tuple, we can iterate over the elements with K in T[number]
, and that's our result done. T
将是["color"]
,而不是string[]
,当然,一旦我们有了元组,我们就可以用K in T[number]
遍历元素,这就是我们完成的结果。
You'll notice that there is PROFILE_TYPE
now instead of PROFILE
.您会注意到现在有PROFILE_TYPE
而不是PROFILE
。 What's that?那是什么?
I changed PROFILE
to this:我将PROFILE
更改为:
const PROFILE_TYPE = {
color: { type: 'string', value: 'red' },
height: { type: 'number', value: 180 },
weight: { type: 'number', value: 70 },
};
const PROFILE = PROFILE_TYPE as Record<string, paramType>;
So you'll have your original PROFILE
, but you also have the original that you can use for typings.因此,您将拥有原始的PROFILE
,但您也拥有可用于打字的原始文件。 The best of both worlds.两全其美的。
And this works amazingly.这非常有效。
Still confused about Narrow
?仍然对Narrow
感到困惑吗?
The way I have used it above is essentially equivalent to the following:我上面使用的方式基本上等同于以下内容:
const constructObj = <T>(selectedFields: [T] extends [[]] ? [] : { [K in keyof T]: Extract<T[K], string> }): T extends string[] ? {
Yeah I don't think it's nice to read or maintain either.是的,我认为阅读或维护都不好。 But again, what it does, is it tests T
is an empty array with [T] extends [[]]
, then if it isn't, it "loops" over the tuple (because looping over it makes TypeScript infer it as a tuple for some reason) and then uses Extract<T[K], string>
to further get TypeScript to infer each individual string as a literal.但同样,它的作用是,它测试T
是一个带有[T] extends [[]]
的空数组,然后如果不是,它在元组上“循环”(因为循环它会使 TypeScript 将其推断为tuple 出于某种原因),然后使用Extract<T[K], string>
进一步让 TypeScript 将每个单独的字符串推断为文字。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.