如何基于另一个 属性 创建具有动态属性的 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维视图中使用,其中n是headers
的长度,但是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>
的对象有一个类型 H
的 headers
属性,它也是(通过 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 obj
与 Obj<["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>
成为泛型,它可能会迫使您将泛型类型参数添加到代码中的其他位置,或者采取其他变通方法来防止这种情况发生。但这超出了所问问题的范围(并且需要很长时间才能完成)所以我将跳过它。
我想创建一个 TypeScript 定义,它使用 headers
中提供的字符串作为嵌套在该类型中的属性的名称,如下所示:
obj = {
headers: ['first', 'second', 'third'],
first: [{
id: 'someStrA',
second: [{
id: 'someStrB',
third: [{
id: 'someStrC'
}],
}],
}]
}
该对象将在n维视图中使用,其中n是headers
的长度,但是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>
的对象有一个类型 H
的 headers
属性,它也是(通过 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 obj
与 Obj<["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>
成为泛型,它可能会迫使您将泛型类型参数添加到代码中的其他位置,或者采取其他变通方法来防止这种情况发生。但这超出了所问问题的范围(并且需要很长时间才能完成)所以我将跳过它。