MappedTypeNode: Add method to generate a structure with enumerated keys?
Is your feature request related to a problem? Please describe.
See issues #758, #1311. Figuring out the implied fields of a MappedTypeNode is hard. As I explain in #1311, I think I have found a way to make this work, and I want to implement it in the MappedTypeNode class directly.
Describe the solution you'd like
I want to provide an API to generate a TypeAliasDeclarationStructure. In my own project, I would add it to a temporary SourceFile in order to extract further type information.
getEnumeratedStructure(name: string) : OptionalKind<TypeAliasDeclarationStructure> {
}
This would do several steps:
- Get the type of a base structure from an ancestor of the node as a string, and extract the mapped type's definition, calling it
templateTypeString. - Get the variable
Identifier's name, and find its reference nodes. - Identify the set of property keys (strings, numbers, symbols) from the mapped type node's constraint type.
- Create a
TypeAliasDeclarationstructure with type "" and the user's passed-in name. Call itenumeratedDeclaration. let typeOutput: string[] = [];- For each element of the set of property keys:
a. Make a copy of
templateTypeString, calling itfieldText. b. For each variable reference node (in reverse order), replace the text of the reference node infieldTextwith the property key (escaped). c.typeOutput.push(" " + key + ": " +fieldText + ";") enumeratedDeclaration.type = "{\n" + typeOutput.join("\n") + "\n};\n";return enumeratedDeclaration;
Ideally, I would add several static and private utility methods to MappedTypeNode to achieve this.
Why a structure?
I don't necessarily want to modify the source files of a target project, except possibly temporary source files I would not save. As far as I know, neither TypeScript nor ts-morph supports detached nodes. A structure is just an ordinary JavaScript object, from what I see.
Describe alternatives you've considered
None so far. This is the first option which will (at least partially) resolve for me the mapped type's fields. I fully expect I will have to work further in my own projects to analyze and optimize the generated type.
Notes on testing
I can already imagine some interesting test cases:
Parameterized mapped types
export type NumberStringType = {
repeatForward<S extends string>(s: S, n: number): string;
repeatBack(n: number, s: string): string;
};
type NumberStringEnumerated<Q extends string> = {
[P in keyof NumberStringType as `${P}${Q}`]: `${Q} = ${P}`;
}
ECMAScript symbols
const SymbolKey = Symbol("key");
type FooBarAndSymbol = {
foo: unknown;
bar: unknown;
[SymbolKey]: boolean;
};
type MappedFooBarAndSymbol = {
[key in keyof FooBarAndSymbol]: key;
};
Conditional types in the mapped type's type node
type FooBar = {
foo: unknown;
bar: unknown;
};
type MappedFooBar = {
[key in keyof FooBar]: key extends "foo" ? true : false;
};
Non-aliased mapped types
type MappedFooBar = {
version: "0.1";
} & {
[key in ("foo" | "bar")]: key;
};
I want to leave it up to other API's to perform the type resolution for indexed access and conditional types, at least for now.
Another test might indicate trouble:
type InfiniteStringKeys = {
[key: string] : 1;
};
type MappedTypeInfiniteStringKeys = {
[key in keyof InfiniteStringKeys]: 2;
};
Both of these have an infinite set of string keys, which would make enumerating them all impossible. We'd have to throw an exception in that case. 🤔
So I am coming up on an issue with performance due to a deeply nested, computed type (lots of mapped types and such throughout) based on some generated code (type-graphql). I feel like having all of the types just dumped into a .d.ts file would be more performant (at least from the language server's perspective) which I think was kind of the original intent of this issue. I don't suppose that any meaningful work was ever done on this?