如何创建类型安全的递归类型?TS树形结构类型定义问题
Let's break down why your current type isn't behaving as expected, then fix it.
The Problem with Your Original Type
Your TypedTree uses a union type (|) between the root schema and a record of child nodes. Union types mean "either/or"—so an empty object {} is valid because it matches the second branch ({ [key: string]: TypedTree<TChildSchema> }, since empty objects are valid for index signatures). That's why your @ts-expect-error test isn't failing like you want.
You actually need an intersection type (&) here: nodes must satisfy the schema and can optionally have additional child nodes (with keys not defined in the schema).
The Fixed Type
Using TypeScript's index remapping (available in 4.4+), we can exclude the schema's keys from the child node index signature to avoid type conflicts:
type TypedTree<TRootSchema, TChildSchema = TRootSchema> = TRootSchema & { // Allow any string key except those already in the root schema [K in string as Exclude<K, keyof TRootSchema>]: TypedTree<TChildSchema>; }; const $typeTest = <T>(v: T) => v; // @ts-expect-error Required properties must be present (this will now fail as expected!) $typeTest<TypedTree<{ a: number }>>({ }) // Valid cases: $typeTest<TypedTree<{ a: number }>>({ a: 5 }) // Root node with only required property $typeTest<TypedTree<{ a: number }>>({ a: 5, child: { a: 10 } }) // Root + child node $typeTest<TypedTree<{ a: number }, { b: string }>>({ a: 5, nested: { b: "test" } }) // Root with custom child schema
How This Works
- Intersection with Schema:
TRootSchema & ...ensures every node (root or child) must include all required properties from the schema. Empty objects fail because they're missing those required fields. - Index Remapping:
[K in string as Exclude<K, keyof TRootSchema>]creates an index signature for any string key not defined in the schema. This prevents conflicts between schema properties and child node keys, while letting you add as many child nodes as needed. - Default Child Schema: The
TChildSchema = TRootSchemadefault means child nodes use the same schema as the root unless you specify otherwise.
Additional Notes
- If you need optional properties in your schema, just mark them as optional in the schema type (e.g.,
{ a: number, b?: string }). The type will respect optional/required status automatically. - This approach ensures every node (root and all descendants) adheres to their respective schema's required/optional rules.
内容的提问来源于stack exchange,提问作者felps321

