如何基于另一个 属性 创建具有动态属性的 TypeScript 类型?

How do I create a TypeScript type with dynamic properties based on another property?

我想创建一个 TypeScript 定义,它使用 headers 中提供的字符串作为嵌套在该类型中的属性的名称,如下所示:

obj = {
  headers: ['first', 'second', 'third'],
  first: [{
    id: 'someStrA',
    second: [{
      id: 'someStrB',
      third: [{
        id: 'someStrC'
      }],
    }],
  }]
}

该对象将在n维视图中使用,其中nheaders的长度,但是n 可以根据所呈现的内容而有所不同。

TypeScript 中没有特定的 Obj 类型与您的一组所需值相对应。 headers 属性 的成员与其他属性的键之间的约束不能直接表达。但是,您可以从 headers 属性 创建 generic Obj<H> type where the H type parameter corresponds to the tuple of string literal types。例如,如果您知道 H["first", "second", "third"],那么 Obj<["first", "second", "third"]> 将是您想要的特定类型。 Obj<H> 可能是这样的:

type Obj<H extends string[]> = { headers: H } & ObjProps<H>

type ObjProps<H> = H extends [infer H0, ...infer HR] ? {
    [P in Extract<H0, string>]: ({ id: string } & ObjProps<HR>)[]
} : unknown

所以类型 Obj<H> 的对象有一个类型 Hheaders 属性,它也是(通过 intersection)一个值输入 ObjProps<H>,负责嵌套。

ObjProps<H> 类型使用 variadic tuple types to split the H type into its first element H0 and the rest of the elements HR. Then ObjProps<H> has a property whose key is H0 (that's the mapped type {[P in Extract<H0, string>]: ...}) and whose value is an array of object types with a string-valued id property which is also an ObjProps<HR> (meaning we recurse down). If H is the empty tuple then ObjProps<H> is the unknown type,这是我们的基本情况。

让我们确保这看起来像我们想要的:

type ConcreteObj = Obj<["first", "second", "third"]>;
/* type ConcreteObj = {
    headers: ["first", "second", "third"];
} & {
    first: ({
        id: string;
    } & {
        second: ({
            id: string;
        } & {
            third: {
                id: string;
            }[];
        })[];
    })[];
} */

呃,有点难看;让我们折叠这些交叉点:

type Expand<T> = T extends object ? { [K in keyof T]: Expand<T[K]> } : T;
type ExpandedConcrete = Expand<ConcreteObj>;
/* type ExpandedConcrete = {
    headers: ["first", "second", "third"];
    first: {
        id: string;
        second: {
            id: string;
            third: {
                id: string;
            }[];
        }[];
    }[];
} */

看起来不错!


当然,您可能不想 annotate objObj<["first", "second", "third"]> 类型。您可能希望编译器从 obj 推断出 ["first", "second", "third"]。您可以借助通用辅助函数来实现这一点:

const asObj = <H extends string[]>(obj: Obj<[...H]>) => obj;

然后不用注释 obj 你只需从 asObj():

的结果推断它的类型
const obj = asObj({
    headers: ['first', 'second', 'third'],
    first: [{
        id: 'someStrA',
        second: [{
            id: 'someStrB',
            third: [{
                id: 'someStrC'
            }],
        }],
    }]
});
// const obj: Obj<["first", "second", "third"]>

这个辅助函数也会发现错误,因为它要求传入的对象是从 headers 属性 推断出的 H 的有效 Obj<H>:

const badObj = asObj({
    headers: ['first', 'second', 'third', 'fourth'],
    first: [{
        id: 'someStrA',
        second: [{
            id: 'someStrB',
            third: [{
                id: 'someStrC' // error!  Property 'fourth' is missing 
            }],
        }],
    }]
});

请注意,通过使 Obj<H> 成为泛型,它可能会迫使您将泛型类型参数添加到代码中的其他位置,或者采取其他变通方法来防止这种情况发生。但这超出了所问问题的范围(并且需要很长时间才能完成)所以我将跳过它。

Playground link to code