英文:
How can I create this recursive arbitrary object?
问题
考虑以下对象:
export const cypher = {
Foo: {
get Bar() {
return 'Foo_Bar';
},
get Baz() {
return 'Foo_Baz';
},
},
get Qux() {
return 'Qux';
},
Lan: {
Rok: {
get Saz() {
return 'Lan_Rok_Saz';
},
},
},
};
你可以看到模式:这是一个“树”,其中每个叶子会返回其整个分支的名称串联字符串,例如:
console.log(cypher.Lan.Rok.Saz) // Lan_Rok_Saz
这个手动对象是严格类型化的,因此可以获得良好的智能感知。现在,我想创建一个接受类似以下类型对象的构造函数:
interface Entries {
[key: string]: (string | Entries)[];
}
并返回具有与上述 cypher
相同结构的对象,以便 TypeScript 可以提供智能感知。内部实现不一定重要,但使用 X.Y.Z
形式肯定是优先考虑的。
到目前为止,我尝试了一个基于值定义属性的递归函数:
interface Entries {
[key: string]: (string | Entries)[];
}
const mockEntries: (string | Entries)[] = ['Gat', 'Bay', {
Foo: ['Bar', 'Baz', { Cal: ['Car'] }],
}];
function buildCypherFromEntries(entries: (string | Entries)[], parentName: string, cypher: Record<string, any> = {}) {
entries.forEach(entry => {
if (typeof entry === 'string') {
Object.defineProperty(cypher, entry, {
get() { return (parentName ? `${parentName}_${entry}` : entry); },
});
} else {
Object.entries(entry).forEach(([key, furtherEntries]) => {
cypher[key] = {};
const furtherParentName = parentName ? `${parentName}_${key}` : key;
buildCypherFromEntries(furtherEntries, furtherParentName, cypher[key]);
})
}
})
return cypher;
}
const c = buildCypherFromEntries(mockEntries, '');
console.log(c.Gat) // Gat
console.log(c.Bay) // Bay
console.log(c.Foo.Bar); // Foo_Bar
console.log(c.Foo.Cal.Car) // Foo_Cal_Car
这个方法可以工作,但不提供智能感知。而且,它也不支持顶级叶子节点,这些叶子节点需要成为 getter。我还尝试以类的形式实现,但是类型定义仍然让我感到困惑。
英文:
Consider this object:
export const cypher = {
Foo: {
get Bar() {
return 'Foo_Bar';
},
get Baz() {
return 'Foo_Baz';
},
},
get Qux() {
return 'Qux';
},
Lan: {
Rok: {
get Saz() {
return 'Lan_Rok_Saz';
},
},
},
};
You can see the pattern: this is a "tree" where each leaf would return a string of the concatanated names of its entire branch, i.e.
console.log(cypher.Lan.Rok.Saz) // Lan_Rok_Saz
This manual object is strictly typed, so I get intellisense nicely.
I would like to now create some constructor, that accepts an object of a type such as:
interface Entries {
[key: string]: (string | Entries)[];
}
And returns an object with the structure as the above cypher
, such that TS would be able to intellisense.
The internal implementation isn't necesarrily important, but the usage of X.Y.Z
is definitely a priority.
So far I've tried a recursive function that defines properties based on values:
interface Entries {
[key: string]: (string | Entries)[];
}
const mockEntries: (string | Entries)[] = ['Gat', 'Bay', {
Foo: ['Bar', 'Baz', { Cal: ['Car'] }],
}];
function buildCypherFromEntries(entries: (string | Entries)[], parentName: string, cypher: Record<string, any> = {}) {
entries.forEach(entry => {
if (typeof entry === 'string') {
Object.defineProperty(cypher, entry, {
get() { return (parentName ? `${parentName}_${entry}` : entry); },
});
} else {
Object.entries(entry).forEach(([key, furtherEntries]) => {
cypher[key] = {};
const furtherParentName = parentName ? `${parentName}_${key}` : key;
buildCypherFromEntries(furtherEntries, furtherParentName, cypher[key]);
})
}
})
return cypher;
}
const c = buildCypherFromEntries(mockEntries, '');
console.log(c.Gat) // Gat
console.log(c.Bay) // Bay
console.log(c.Foo.Bar); // Foo_Bar
console.log(c.Foo.Cal.Car) // Foo_Cal_Car
This works, but does not give any intellisense.
It's also not perfect as it does not support top-level leaves that turn into getters.
I also tried doing this in class form but again the typing confounds me.
答案1
得分: 2
为了使这个可能起作用,你不能将 `mockEntries` 注释为类型 `(string | Entries)[]`,因为这会完全丢弃编译器从初始化程序中推断出的任何更具体的信息。相反,你应该使用 [`const` 断言](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions) 来获取关于值的最具体信息,特别是 `string` [文字类型](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types):
```typescript
const mockEntries = ['Gat', 'Bay', {
Foo: ['Bar', 'Baz', { Cal: ['Car'] }],
}] as const;
/* const mockEntries: readonly ["Gat", "Bay", {
readonly Foo: readonly ["Bar", "Baz", {
readonly Cal: readonly ["Car"];
}];
}] */
现在我们可以继续。请注意,这些数组是 readonly
元组类型;我们不太关心顺序或 readonly
(尽管最好不要更改该信息),但这意味着如果在我们的类型中允许 readonly
数组,事情将变得最容易。为此,让我们重新定义 Entries
如下:
interface Entries {
[key: string]: readonly (string | Entries)[];
}
一种方法如下:
declare function buildCypherFromEntries<T extends readonly (string | Entries)[], U = {}>(
entries: T, parentName: string, cypher?: U
): U & BuildCypherFromEntryElements<T>;
type BuildCypherFromEntryElements<T> =
T extends readonly any[] ?
BuildCypherFromEntries<UnionToIntersection<StrToObj<T[number]>>> :
string
type StrToObj<T> =
T extends string ? { [P in T]: undefined } : T;
type UnionToIntersection<T> =
(T extends infer U ? ((x: U) => void) : never) extends
((x: infer I) => void) ? I : never;
type BuildCypherFromEntries<T> = {
-readonly [K in keyof T]: BuildCypherFromEntryElements<T[K]>
} extends infer O ? { [K in keyof O]: O[K] } : never;
我们需要使 buildCypherFromEntries()
中的 entries
参数的类型 T
成为 泛型,还有可能是 cypher
参数的类型 U
,我们将其默认为空对象类型 {}
。函数的返回类型是 cypher
类型 U
与类型 BuildCypherFromEntryElements<T>
的 交叉类型。U
部分只保留可能存在于 U
中的任何其他属性,而主要工作在 BuildCypherFromEntryElements
中进行。
因此,BuildCypherFromEntryElements<T>
接受一个预期为数组的 T
,但如果我们递归到一个单独的字符串而不是对象,则可能是“空的”。如果它不是一个数组,我们只需输出 string
作为值类型。如果它是一个数组,那么我们首先将 StrToObj<>
应用于其元素的 联合类型,以便将字符串转换为具有正确键和我们不关心的值的东西(例如,我们将 'Bar' | 'Baz' | { Cal: ['Car'] }
转换为 {Bar: undefined} | {Baz: undefined} | {Cal: ['Car']}
)。这通过 分布式条件类型 来实现。然后我们将 UnionToIntersection<>
应用于它(参见 https://stackoverflow.com/q/50374908/2887218 以获取实现信息),将联合转换为交集(例如,{Bar: undefined} & {Baz: undefined} & {Cal: ['Car']}
)。最后,我们将 BuildCypherFromEntries<T>
应用于其中。
而 BuildCypherFromEntries<T>
是对其输入的 映射类型,只需将 BuildCypherFromEntryElements<>
应用于每个属性。我还使用了一个技巧,将生成的嵌套交集类型转换为更美观的形式(参见 https://stackoverflow.com/q/57683303/2887218 以获取实现信息)。
让我们来测试一下:
const c = buildCypherFromEntries(mockEntries, '');
/* const c: {
Foo: {
Cal: {
Car: string;
};
Bar: string;
Baz: string;
};
Gat: string;
Bay: string;
} */
看起来符合你的要求!
<details>
<summary>英文:</summary>
In order for this to possibly work, you can't [annotate](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-annotations-on-variables) `mockEntries` as having type `(string | Entries)[]`, since that completely throws away any more specific information the compiler might infer from the initializer. Instead you should use a [`const` assertion](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions) to get the most specific information about the value as possible, especially the `string` [literal types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types):
const mockEntries = ['Gat', 'Bay', {
Foo: ['Bar', 'Baz', { Cal: ['Car'] }],
}] as const;
/* const mockEntries: readonly ["Gat", "Bay", {
readonly Foo: readonly ["Bar", "Baz", {
readonly Cal: readonly ["Car"];
}];
}] */
Now we can proceed. Note that the arrays are [`readonly` tuple types](https://www.typescriptlang.org/docs/handbook/2/objects.html#readonly-tuple-types); we don't care much about the ordering or the `readonly`-ness (although it's probably for the best that you don't alter that information), but that does mean things will be easiest if we allow `readonly` arrays in our types. To that end, let's redefine `Entries` as:
interface Entries {
[key: string]: readonly (string | Entries)[];
}
---
One approach looks like this:
declare function buildCypherFromEntries<T extends readonly (string | Entries)[], U = {}>(
entries: T, parentName: string, cypher?: U
): U & BuildCypherFromEntryElements<T>;
type BuildCypherFromEntryElements<T> =
T extends readonly any[] ?
BuildCypherFromEntries<UnionToIntersection<StrToObj<T[number]>>> :
string
type StrToObj<T> =
T extends string ? { [P in T]: undefined } : T;
type UnionToIntersection<T> =
(T extends infer U ? ((x: U) => void) : never) extends
((x: infer I) => void) ? I : never;
type BuildCypherFromEntries<T> = {
-readonly [K in keyof T]: BuildCypherFromEntryElements<T[K]>
} extends infer O ? { [K in keyof O]: O[K] } : never;
We need `buildCypherFromEntries()` to be [generic](https://www.typescriptlang.org/docs/handbook/2/generics.html) in the type `T` of the `entries` argument, plus I guess the type `U` of the `cypher` argument which we'll default to the empty object type `{}`. The return type of the function is the [intersection](https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types) of the `cypher` type `U` with the type `BuildCypherFromEntryElements<T>`. The `U` part just keeps any other properties that might be in `U`, while the main work happens inside `BuildCypherFromEntryElements`.
So `BuildCypherFromEntryElements<T>` takes a `T` which is expected to be an array but might also be "empty" if we've recursed down into a single string instead of an object. If it's not an array we just output `string` as the value type. If it *is* an array, then we first apply `StrToObj<>` to the [union](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types) of its elements, so that strings are turned into something with the right key and a value we don't care about (e.g., we turn `'Bar' | 'Baz' | { Cal: ['Car'] }` into `{Bar: undefined} | {Baz: undefined} | {Cal: ['Car']}`). This works via [distributive conditional type](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types). Then we apply `UnionToIntersection<>` to that (see https://stackoverflow.com/q/50374908/2887218 for implementation information), to turn the union into an intersection (e.g., `{Bar: undefined} & {Baz: undefined} & {Cal: ['Car']}`. And finally we apply `BuildCypherFromEntries<T>` to that.
And `BuildCypherFromEntries<T>` is a [mapped type](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html) over its input that just applies `BuildCypherFromEntryElements<>` to each property. Well I also use a trick to turn the resulting nested intersection type into something pretty (see https://stackoverflow.com/q/57683303/2887218 for implementation info).
----
Let's test it out:
const c = buildCypherFromEntries(mockEntries, '');
/* const c: {
Foo: {
Cal: {
Car: string;
};
Bar: string;
Baz: string;
};
Gat: string;
Bay: string;
} */
That looks like what you wanted!
[Playground link to code](https://www.typescriptlang.org/play?ts=5.1.6#code/MYewdgzgLgBMCeAHAFgUwE4wLwwN4CgYYAxEEALj0KJgHNVYAhAQ3QAoBKKmm9BgV3RgYAclIgA+i3QiA3NSIBfADQK6DGCwBenbjxh8og4WLJTmWuWpXUbRerACK-AB66CPQ8dHOXVpapEADLMYJQeNABKIADW4Wr2GgDKFu4JvAJCoiFgEtExEimW8vowdjTlNory+ACWYFAYAGbMwKgwAKIN6LWoEHoA2jGo8JTQPWC0ALqUfMwAJuAANvAwbOP1tDAAPp3dvRAcA1Pyivj4UEjtjPy1S-MAwkho6MToIAC2XVA9fQA8ABUAHzYPQAWjmizAKxgAwA0jB6jBhvAQE0YACZppbvcnigMG9Pt90PAOktUB9UA0IID4VMgfhFDBUC5GmB5v16k0MDAAPIwAD8eFhCKRKLRfKxvLpZRglDAqAAbhgapdENccY9ngT3l9uqTyZTqYCQVhqADmayqRyDKgFstVqF4MdBdQbnctfjXrriQc-gBVMC1cAAkAASQaGAgqGAUGDYD+SR+od5ACMAFa0sD8D6pjD0oEg8jUDaTc5q9qB+OhiONdDR2Pxk3YahsC0stk2rk8-2CtZsFyUf1cLAgxUgWrzLjypUYLgd60QVsDyjdzBhkdjidTvthuUwBXK9Cqq4wJPoFMZ5tmojtq3s-qlrZC3CwgAKiOEmMo-HZqCa9SoPMsqUACNRNL+jbgDAqaaniLyEnqPx+nenb9JCDprE+Ox7MhfRHFMygwL2OC4IoQJsNQVJ4RAoFEYgrDUQAcswlJjMhkxEQgXoCkO+AcEOMAAGTYh68E6kS+pkhS1E0sC4GQXG0GwWJ2repJNGUUQ1G-LRtr2tCqzrBxWy7L6+HHIEMAMXwDQsWxMBPlZ3EvJQkQxiA6DzH8TkwE6pp4GcXARDpBwAHRNJ5HStMgbA6aso56EQtTomwFYSvF2BYDgIhPiIwXpAA9IVMBQMgtScv0yCsMBUAgDA8yoLBWwQPVYYiB8MDpvw0C2ogSytJsiKwAA7rUZXWe86roJcjkMFZPDFQeICwMA5KsDAI3IKsqL8CIwGzsIiyleVEBERA9RtCdfTtIxjkmf0W21MAyANfVYArXA1WTKgRUAFRpumMZQGFjUAQqb5TRglxsC5GBEfFRERKUDjuLaRhZGwNnMax7RCgABgAJLg2N2bjigSMT8WKPj+7xRwshlAtFQM39hXpHD6ADPFUygqTUD2XjMBEyTjFk5SFNU-qNN0-qJQVMySzRklNCA8DYWhX0cX6hwEVRTFbBsEMIxERBM0vOZEBTJuKs8JzxvwLzpHVOkRCgJAsBm2VGBvmLAu43zfuC32Iv84Lku4CiMuUCi8ulCpuJqYhltsF7Fv7H0puCN76C+7Z-uUlxakO9bccs+cPBnEoHDUF4WSc6c5zu71HwgMAMSW6CAwiAA4swUAiERIgsPAg96OIlDd9IY-DxYY+vg8zBLJPIiLzIvOKIRjK88w-TN1ANSFX9cDgC3bcdxnekYYZsIAER91At9EbfI9Pyr18whP+lQjCAwv6wb9-5aDfsjfQH9ViL2Xt-TCf8163xONYBBm8YBsybqfVaoIE6egQj6S+bBW7t0tkPfK8gj4nw9nAeIPAv6gJ4JAqhpR9Br3YhMWgZdK7sJoNIFhmxOFEG0DwyYZcXY8AfoIthagR7iNOCg9mzcQDkjCksEAtBYZhXEGFaQDMYBLXEOYdA+B5GKOUao4AHAgA)
</details>
# 答案2
**得分**: 1
[jcalz已经解释得非常好](https://stackoverflow.com/a/76807262/6567275) 比我能做的更好。
我只想添加一个稍微不同的实现:
```Typescript
interface Entries {
[key: string]: readonly (string | Entries)[];
}
/** {foo: 1} | {bar: 2} -> {foo: 1, bar: 2} */
type Merge<T> = (T extends object ? (x: T) => void : never) extends ((x: infer R) => void) ? { [K in keyof R]: R[K] } : never;
type CypherFromEntries<
T extends string | Entries | Entries[string],
Prefix extends string = ""
> =
T extends string ? { readonly [K in T]: `${Prefix}${T}` } :
T extends Entries ? { -readonly [K in string & keyof T]: CypherFromEntries<T[K], `${Prefix}${K}_`> } :
T extends Entries[string] ? Merge<CypherFromEntries<T[number], Prefix>> :
never;
// 很遗憾,我不得不使用 excessive Force (any),以便 TS 不会卡在其 "excessively deep and possibly infinite" 的类型中。
function cypherFromEntries<
const T extends string | Entries | Entries[string],
const Prefix extends string = ""
>(value: T, prefix: Prefix = "" as Prefix): CypherFromEntries<T, Prefix> {
const _ = (acc: any, item: string | Entries) => {
if (typeof item === "string") {
acc[item] = `${prefix}${item}`;
} else {
for (let key in item) {
acc[key] = (cypherFromEntries as any)(item[key], `${prefix}${key}_`);
}
}
return acc;
}
return Array.isArray(value) ?
(value as Entries[string]).reduce(_, {} as any) :
_({}, value as string | Entries);
}
示例:
const c = cypherFromEntries(['Gat', 'Bay', {
Foo: ['Bar', 'Baz', { Cal: ['Car'] }],
}]);
/*
const c: {
readonly Gat: "Gat";
readonly Bay: "Bay";
Foo: {
readonly Bar: "Foo_Bar";
readonly Baz: "Foo_Baz";
Cal: {
readonly Car: "Foo_Cal_Car";
};
};
}
*/
英文:
jcalz already explained all the why's way better than I can.
I just want to add a slightly different implementation:
interface Entries {
[key: string]: readonly (string | Entries)[];
}
/** {foo: 1} | {bar: 2} -> {foo: 1, bar: 2} */
type Merge<T> = (T extends object ? (x: T) => void : never) extends ((x: infer R) => void) ? { [K in keyof R]: R[K] } : never;
type CypherFromEntries<
T extends string | Entries | Entries[string],
Prefix extends string = ""
> =
T extends string ? { readonly [K in T]: `${Prefix}${T}` } :
T extends Entries ? { -readonly [K in string & keyof T]: CypherFromEntries<T[K], `${Prefix}${K}_`> } :
T extends Entries[string] ? Merge<CypherFromEntries<T[number], Prefix>> :
never;
// sadly I have to use excessive Force (any) so that TS doesn't
// get hung up in its "excessively deep and possibly infinite" typings.
function cypherFromEntries<
const T extends string | Entries | Entries[string],
const Prefix extends string = ""
>(value: T, prefix: Prefix = "" as Prefix): CypherFromEntries<T, Prefix> {
const _ = (acc: any, item: string | Entries) => {
if (typeof item === "string") {
acc[item] = `${prefix}${item}`;
} else {
for (let key in item) {
acc[key] = (cypherFromEntries as any)(item[key], `${prefix}${key}_`);
}
}
return acc;
}
return Array.isArray(value) ?
(value as Entries[string]).reduce(_, {} as any) :
_({}, value as string | Entries);
}
Example:
const c = cypherFromEntries(['Gat', 'Bay', {
Foo: ['Bar', 'Baz', { Cal: ['Car'] }],
}]);
/*
const c: {
readonly Gat: "Gat";
readonly Bay: "Bay";
Foo: {
readonly Bar: "Foo_Bar";
readonly Baz: "Foo_Baz";
Cal: {
readonly Car: "Foo_Cal_Car";
};
};
}
*/
One important difference to your code: I use a prefix
where you use the parentName
. Basically
prefix === parentName ? `${parentName}_` : "";
// that way I can replace
parentName ? `${parentName}_${key}` : key
// with
prefix + key
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论