fix: inline purely structural generics#2293
Conversation
…works not in all cases
| if (!(type instanceof AliasType)) { | ||
| return false; | ||
| } | ||
| if (!node.typeParameters?.length) { |
There was a problem hiding this comment.
| if (!node.typeParameters?.length) { | |
| if (node.typeParameters?.length == 0) { |
There was a problem hiding this comment.
Hmm, with this, some other tests fail since undefined != 0 but non‑generic type aliases like type PrivateAnonymousTypeLiteral = { … } have node.typeParameters set to undefined and the function returns false in that case.
| return /[\\/]typescript[\\/]lib[\\/]/i.test(sourceFile.fileName); | ||
| } | ||
|
|
||
| private shouldInline(node: ts.Node, type: BaseType, context: Context): boolean { |
There was a problem hiding this comment.
How does this handle cases where a symbol is created that can be used in multiple cases? Then it would be beneficial to have the alias, no?
There was a problem hiding this comment.
Do you mean a case like this?
export type MyHelper<A, B> = {
[K in keyof A as K extends keyof B ? never : K]: A[K];
} & B;
type Base = { foo: string; bar: number };
type Patch = { bar: string; baz: boolean };
type Resolved = MyHelper<Base, Patch>; // ← `Resolved` NOT EXPORTED
export interface Foo {
beta: Resolved;
}
export interface Bar {
gamma: Resolved;
}which previously produced
before
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"Bar": {
"additionalProperties": false,
"properties": {
"gamma": {
"$ref": "#/definitions/MyHelper<alias-550436553-91-134-550436553-0-329,alias-550436553-134-178-550436553-0-329>"
}
},
"required": ["gamma"],
"type": "object"
},
"Foo": {
"additionalProperties": false,
"properties": {
"beta": {
"$ref": "#/definitions/MyHelper<alias-550436553-91-134-550436553-0-329,alias-550436553-134-178-550436553-0-329>"
}
},
"required": ["beta"],
"type": "object"
},
"MyHelper<alias-550436553-91-134-550436553-0-329,alias-550436553-134-178-550436553-0-329>": {
"additionalProperties": false,
"properties": {
"bar": { "type": "string" },
"baz": { "type": "boolean" },
"foo": { "type": "string" }
},
"required": ["bar", "baz", "foo"],
"type": "object"
}
}
}and now produces
after
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"Bar": {
"additionalProperties": false,
"properties": {
"gamma": {
"additionalProperties": false,
"properties": {
"bar": { "type": "string" },
"baz": { "type": "boolean" },
"foo": { "type": "string" }
},
"required": ["bar", "baz", "foo"],
"type": "object"
}
},
"required": ["gamma"],
"type": "object"
},
"Foo": {
"additionalProperties": false,
"properties": {
"beta": {
"additionalProperties": false,
"properties": {
"bar": { "type": "string" },
"baz": { "type": "boolean" },
"foo": { "type": "string" }
},
"required": ["bar", "baz", "foo"],
"type": "object"
}
},
"required": ["beta"],
"type": "object"
}
}
}?
In this case, all that's needed to enable reuse is to export type Resolved; then the generator will emit a single definition with $refs. If the user doesn’t control the source, they probably don’t want those verbose alias-550436553-… identifiers in their schema anyway. They may make the schema slightly smaller, but is there any real benefit to having it cluttered with definitions like that?
There was a problem hiding this comment.
Hmm, we have a principle to reuse definitions (even if not explicitly exported). It helps with generating code in other programming languages for example.
I wonder whether we can take a different approach. We could check the generated schema and inline aliases that are generated (rather than inferred from type names) for those cases where the alias is only used once. This could be something that covers the original case you described as well as other cases.
Thoughts?
There was a problem hiding this comment.
Like a simple post-processing pass? That's exactly what I do in my own projects with this lib to strip out the generic helper types, so yes - totally feasible.
The one thing: in the example I showed earlier we'd still end up with a definition called
MyHelper<alias-550436553-91-134-550436553-0-329,alias-550436553-134-178-550436553-0-329>, right? Would you want to keep it as-is?
And do you think it's worth exploring a way to rename such definitions to a clearer name like MyHelper<Base, Patch> instead of those long alias-… placeholders? Curious whether you see that as doable.
There was a problem hiding this comment.
It wouldn't be post processing on the json but somewhere later in the flow. I think we have some deduplication logic if I recall correctly where this could happen.
Renaming aliases makes sense but would also need to be a separate pass when we can ensure that we don't have collisions. I'm less worried about that since you can export your alias if you do care about its name.
Co-authored-by: Dominik Moritz <domoritz@gmail.com>
| if (!sourceFile) { | ||
| return false; | ||
| } | ||
| return /[\\/]typescript[\\/]lib[\\/]/i.test(sourceFile.fileName); |
There was a problem hiding this comment.
will this work in windows?
There was a problem hiding this comment.
As far as I can see, yes, an example path to one of the TypeScript lib .d.ts files is:
...\AppData\Local\Programs\Microsoft VS Code\resources\app\extensions\node_modules\typescript\lib\lib.es5.d.ts
However, this will probably lose relevance if we implement a different approach, as @domoritz suggested above. To be able to keep definitions even with auto-generated generic names when they are reused more than once, we will need to process them after all definitions have been collected, as an additional step in SchemaGenerator.createSchemaFromNodes, I guess. At that point, we have no information about which file the type of a definition came from.
So we would need to rely only on detecting names like object-..., alias-..., and possibly identifying generics simply by observing < in their names (if we agree to add the latter, some of the existing tests will need to update the expected schema, since now having a generic-name definition is expected, even if it's used only once, and few tests reflect it, including vega-lite).
I have some draft solutions but had little time to finish them. I'll come back to this soon, but if you have any comments on what I said above, they're very welcome.
There was a problem hiding this comment.
You might be able to add the path information in an intermediate step before producing the final json so we don't have to rely on reverse engineering from names.
… desired: 1) don't inline reused 2) proper naming TODO: - Add test case that does the same but with different non-exported objects in other file (to ensure collisions are resolved) - Add test case that does the same but uses object in place instead of 'Base' or 'Patch' object
Partially closes #2233
When a type alias with generic parameters ultimately resolves to a purely structural type (no remaining type-parameters, no public reusable symbol), emitting it as a separate
$refjust clutters the schema tree.This PR detects those cases and inlines the resolved structure directly into the parent definition, collapsing long reference chains and trimming unused
definitions.Examples
Case 1 — Mapped/Override helper
Before
{ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "Base": { /* ... */ }, "Merge<Base,structure-303496744-155-188-303496744-125-190-303496744-103-191-303496744-0-192>": { "$ref": "#/definitions/Simplify%3C(alias-1249507601-457-604-1249507601-0-1064%3Cdef-alias-1249507601-129-293-1249507601-0-1064%3Cdef-alias-303496744-44-103-303496744-0-192%3E%2Cdef-alias-1249507601-129-293-1249507601-0-1064%3Cstructure-303496744-155-188-303496744-125-190-303496744-103-191-303496744-0-192%3E%3E%26alias-1249507601-457-604-1249507601-0-1064%3Cdef-alias-1249507601-293-457-1249507601-0-1064%3Cdef-alias-303496744-44-103-303496744-0-192%3E%2Cdef-alias-1249507601-293-457-1249507601-0-1064%3Cstructure-303496744-155-188-303496744-125-190-303496744-103-191-303496744-0-192%3E%3E)%3E" }, "MyType": { "$ref": "#/definitions/OverrideProperties%3CBase%2Cstructure-303496744-155-188-303496744-125-190-303496744-103-191-303496744-0-192%3E" }, "OverrideProperties<Base,structure-303496744-155-188-303496744-125-190-303496744-103-191-303496744-0-192>": { "$ref": "#/definitions/Merge%3CBase%2Cstructure-303496744-155-188-303496744-125-190-303496744-103-191-303496744-0-192%3E" }, "Simplify<(alias-1249507601-457-604-1249507601-0-1064<def-alias-1249507601-129-293-1249507601-0-1064<def-alias-303496744-44-103-303496744-0-192>,def-alias-1249507601-129-293-1249507601-0-1064<structure-303496744-155-188-303496744-125-190-303496744-103-191-303496744-0-192>>&alias-1249507601-457-604-1249507601-0-1064<def-alias-1249507601-293-457-1249507601-0-1064<def-alias-303496744-44-103-303496744-0-192>,def-alias-1249507601-293-457-1249507601-0-1064<structure-303496744-155-188-303496744-125-190-303496744-103-191-303496744-0-192>>)>": { "additionalProperties": false, "properties": { "bar": { "type": "string" }, "foo": { "type": "string" } }, "required": ["bar", "foo"], "type": "object" } } }After
{ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "Base": { /* ... */ }, "MyType": { "additionalProperties": false, "properties": { "bar": { "type": "string" }, "foo": { "type": "string" } }, "required": ["bar", "foo"], "type": "object" } } }Case 2 —
ValueOfon a const objectBefore
{ "$ref": "#/definitions/MyType", "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "MyType": { "$ref": "#/definitions/ValueOf%3Cobject-810933776-28-72-810933776-28-81-810933776-12-81-810933776-6-81-810933776-0-82-810933776-0-174%3E" }, "ValueOf<object-810933776-28-72-810933776-28-81-810933776-12-81-810933776-6-81-810933776-0-82-810933776-0-174>": { "enum": ["foo-val", "bar-val"], "type": "string" } } }After
{ "$ref": "#/definitions/MyType", "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "MyType": { "enum": ["foo-val", "bar-val"], "type": "string" } } }Inlining these internal generics is safe: the generator already skips emitting them as standalone definitions - even if you'd request
type: "OverrideProperties"- so this change only cleans the schema output without dropping any functionality.