简体   繁体   English

基于数组动态键入 object?

[英]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

Playground link 游乐场链接

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM