This package, ts-morph-structures
, provides helper utilities for building stub classes out of existing TypeScript interfaces and object literals.
As you might imagine, such a task is not trivial. There’s a lot more in the class than in the combination of the interface and the object literal. So how do we get from interfaces to classes?
Some of this may seem obvious to any programmer reading this, but it’s important to lay out the foundations for why these utilities follow a specific pattern. If these assumptions don’t hold true for your use case, then you may not want to use all of the tools in this particular box.
Properties of a class represent the actual data of the class. They are what’s specific to a particular instance of a class - or for static properties, what’s specific to the class itself. Everything else in the class builds on top of them (and the base class, of course).
In ts-morph-structures, the PropertyDeclarationImpl
represents a property structure, and has specific (pun not intended) properties:
For type-to-class purposes, we’re concerned mostly with “name”, “type” and “initializer”.
The first part of this is obvious to any programmer, but the most important part here is in the last word. Without statements (in a particular order), methods are useless. So are constructors, getters and setters.
By “purpose”, I mean “what are we doing in this part?” It could be one of several tasks:
Here I’m glossing over “return as early as you can”.
Yes, four dimensions of statement complexity:
This becomes really important later (“ClassFieldStatementsMap”).
InterfaceDeclarationImpl
| MemberedObjectTypeStructureImpl
Both of these implement ts-morph’s TypeElementMemberedNodeStructure
interface, which has several properties:
callSignatures
: CallSignatureDeclarationImpl[]
, (x: string): void;
constructSignatures
: ConstructSignatureDeclarationImpl[]
, new (x: string): SomeObject
getAccessors
: GetAccessorDeclarationImpl[]
, get y(): number
indexSignatures
: IndexSignatureDeclarationImpl[]
, [key: string]: boolean;
methods
: MethodSignatureImpl[]
, doSomething(value: string): void
properties
: PropertySignatureImpl[]
, color: string
setAccessors
: SetAccessorDeclarationImpl[]
, set y(value: number);
Class structures have a different, partially compatible interface: ClassDeclarationImpl
.
ctors
: ConstructorDeclarationImpl[]
: constructor(color: string) {/* ... */}
ConstructSignatureDeclarationImpl
above, but not in this context right now. I very much build my own here.getAccessors
: GetAccessorDeclarationImpl[]
methods
: MethodDeclarationImpl[]
properties
: PropertyDeclarationImpl[]
setAccessors
: SetAccessorDeclarationImpl[]
MemberedTypeToClass
and its helpers try to bridge this specific gap.
TypeMembersMap
A membered object is ideal for serializing (writer functions not withstanding), but for getting a specific member, or for defining one, it’s less direct. TypeMembersMap
extends Map<string, TypeMemberImpl>
with the following definition:
export type TypeMemberImpl = (
CallSignatureDeclarationImpl |
ConstructSignatureDeclarationImpl |
GetAccessorDeclarationImpl |
IndexSignatureDeclarationImpl |
MethodSignatureImpl |
PropertySignatureImpl |
SetAccessorDeclarationImpl
);
That said, the specific keys of this map are not necessarily the name of the member you have in mind. Index signatures don’t have names, for example. It’s also easy to have a conflict between get foo()
and a foo
property. For this, TypeMembersMap
provides two static methods:
static keyFromMember(member: TypeMemberImpl): string
andstatic keyFromName(kind: TypeMemberImpl["kind"], name: string): string
The basic algorithm for creating a key is simple:
Example: typeMembers.has(TypeMembersMap.keyFromMember(myProperty));
There are variations for constructors, index signatures and call signatures.
I do not recommend direct access to the map’s inherited methods from Map
unless you fully understand this algorithm.
For convenience, if you already have a membered object, TypeMembersMap
has another method.
static fromMemberedObject(membered): TypeMembersMap
Individual maps have specific helper methods:
addMembers(members: TypeMemberImpl[]): void
arrayOfKind<Kind extends TypeMemberImpl["kind"]>(kind: Kind)
clone(): TypeMembersMap
convertAccessorsToProperty(name: string): void
convertPropertyToAccessors(name: string, toGetter: boolean, toSetter: boolean): void
getAsKind<Kind extends TypeMemberImpl["kind"]>(kind: Kind, name: string)
moveMembersToType(owner: InterfaceDeclarationImpl | MemberedObjectTypeStructureImpl): void
resolveIndexSignature(signature: IndexSignatureDeclarationImpl, names: string[]): void
sortEntries(comparator: (a: [string, TypeMemberImpl], b: [string, TypeMemberImpl]) => number): void;
The resolveIndexSignature()
method needs some explanation. Index signatures represent methods and properties, but with variable names for the methods and properties. Classes require concrete names. This method lets you provide the concrete names to replace the index signature with.
ClassMembersMap
Similar to TypeMembersMap
, ClassMembersMap extends Map<string, ClassMemberImpl>
.
export type ClassMemberImpl = (
ConstructorDeclarationImpl |
PropertyDeclarationImpl |
GetAccessorDeclarationImpl |
SetAccessorDeclarationImpl |
MethodDeclarationImpl
);
The key algorithm is similar as well. The methods for generating keys are:
static keyFromMember(member: ClassMemberImpl): string
static keyFromName(kind: ClassMemberImpl["kind"], isStatic: boolean, name: string,): string
The algorithm for generating a key is:
Other static methods:
fromClassDeclaration(classDecl: ClassDeclarationImpl): ClassMembersMap
convertTypeMembers(isStatic: boolean, typeMembers: NamedTypeMemberImpl[], map?: WeakMap<ClassMemberImpl, TypeMemberImpl>): NamedClassMemberImpl[]
The class member map’s non-static methods are similar too:
addMembers(members: readonly ClassMemberImpl[]): void;
arrayOfKind<Kind extends ClassMemberImpl["kind"]>(kind: Kind);
moveMembersToClass(classDecl: ClassDeclarationImpl): ClassDeclarationImpl;
clone(): ClassMembersMap;
convertAccessorsToProperty(isStatic: boolean, name: string): void;
convertPropertyToAccessors(isStatic: boolean, name: string, toGetter: boolean, toSetter: boolean);
getAsKind<kind extend ClassMemberImpl["kind"]>(kind: Kind, key: string);
moveStatementsToMembers(statementMaps: ClassFieldStatementsMap[]): void;
sortEntries(comparator: (a: [string, ClassMemberImpl], b: [string, ClassMemberImpl]) => number): void;
The moveStatementsToMembers()
method requires an explanation of ClassFieldStatementsMap
.
ClassFieldStatementsMap
Consider the following example:
class RedAndBluePlayers {
#redPoints: number;
#bluePoints: number;
constructor(redPoints: number, bluePoints: number) {
}
public movePointFromRedToBlue() {
}
public movePointFromBlueToRed() {
}
}
Everything above we can get from a TypeMembersMap
, converting to a ClassMembersMap
(and adding the constructor to the class map). What we can’t get are the function bodies. There’s a number of statements to consider:
this.#redPoints
greater than zero? If not, throw.this.#bluePoints
.this.#redPoints
.this.#bluePoints
greater than zero? If not, throw.this.#redPoints
.this.#bluePoints
.We could capture this as follows:
const statementsMap = new ClassFieldStatementsMap();
statementsMap.set("_check", "movePointFromRedToBlue", [
`if (this.#redPoints <= 0) throw new Error("no red points to move");`,
]);
statementsMap.set("_check", "movePointFromBlueToRed", [
`if (this.#bluePoints <= 0) throw new Error("no blue points to move");`,
]);
statementsMap.set("redPoints", "movePointFromRedToBlue", [
`this.#redPoints--;`,
]);
statementsMap.set("bluePoints", "movePointFromBlueToRed", [
`this.#bluePoints++;`,
]);
statementsMap.set("redPoints", "movePointFromBlueToRed", [
`this.#redPoints++;`,
]);
statementsMap.set("bluePoints", "movePointFromBlueToRed", [
`this.#bluePoints--;`,
]);
statementsMap.set("redPoints", "constructor", [
`this.#redPoints = redPoints;`,
]);
statementsMap.set("bluePoints", "constructor", [
`this.#bluePoints = bluePoints;`,
]);
From the above, the class members map could then generate the following code:
class RedAndBluePlayers {
#redPoints: number;
#bluePoints: number;
constructor(redPoints: number, bluePoints: number) {
this.#bluePoints = bluePoints;
this.#redPoints = redPoints;
}
public movePointFromRedToBlue() {
if (this.#redPoints <= 0) throw new Error("no red points to move");
this.#bluePoints++;
this.#redPoints--;
}
public movePointFromBlueToRed() {
if (this.#bluePoints <= 0) throw new Error("no blue points to move");
this.#bluePoints--;
this.#redPoints++;
}
}
This is what ClassFieldStatementsMap
is all about.
Earlier, I mentioned four dimensions of complexity for statements. ClassFieldStatementsMap
handles three of them:
ClassFieldStatementsMap
is a two-keyed map, with similar API to Map<string, ClassFieldStatement[]>
. It’s like saying Map<string, string, ClassFieldStatement[]>
, although this would be an illegal map definition. (I derived it from my “composite-collection” library, which generates multi-keyed maps and sets.)
ClassFieldStatementsMap.FIELD_HEAD_SUPER_CALL
for statements that must be at the head of a statement group. (Example: super.doSomething();
)ClassFieldStatementsMap.FIELD_TAIL_FINAL_RETURN
for statements that must be at the tail of a statement group. (Example: return true;
)ClassFieldStatementsMap.GROUP_INITIALIZER_OR_PROPERTY
. For properties, this is an initializer for the property. (Example: foo: number = 5;
) For getters and setters, this represents a value to mirror. (Example: return this.#foo;
, this.#foo = value;
)export type ClassFieldStatement = string | WriterFunction | StatementStructureImpls;
export type StatementStructureImpls =
| ClassDeclarationImpl
| EnumDeclarationImpl
| ExportAssignmentImpl
| ExportDeclarationImpl
| FunctionDeclarationImpl
| ImportDeclarationImpl
| InterfaceDeclarationImpl
| ModuleDeclarationImpl
| TypeAliasDeclarationImpl
| VariableStatementImpl;
Beyond the standard methods of a Map
, there are two additional methods specific to statement groups (the second key):
groupKeys(): string[]
, returning all the statement group keys.groupStatementsMap(statementGroup: string): ReadonlyMap<string, ClassFieldStatement[]> | undefined
, returning all the field names and statement arrays for a given statement group.ClassFieldStatementsMap
exposes other features:
purposeKey?: string;
: the purpose key (though this is just a placeholder)regionName?: string;
: //#region
and //#endregion
comments around the block for code foldingisBlockStatement: boolean
: if true, enclose all statements from the map in curly braces.Other useful methods:
static normalizeKeys(fieldName: string, statementGroup: string): [string, string];
static fieldComparator(a: string, b: string): number;
(for sorting statements by field name)MemberedTypeToClass
: your driver for creating stub classesNow we get to the center of it all: the MemberedTypeToClass
class. Primarily, it has a few tasks, in order:
importFromMemberedType()
, importFromTypeMembersMap()
, addTypeMember()
)new MemberedTypeToClass
)indexSignatureResolver
)isAbstractCallback
)isAsyncCallback
)isGeneratorCallback
)scopeCallback
)isBlockStatement
, optional regionName
)insertMemberKey
)buildClassMembersMap()
)Building a class declaration is trivial, once you have the class members map. (.moveMembersToClass(classDecl)
)
declare class MemberedTypeToClass {
importFromMemberedType(
isStatic: boolean,
membered: InterfaceDeclarationImpl | MemberedObjectTypeStructureImpl,
): void;
importFromTypeMembersMap(
isStatic: boolean,
membersMap: TypeMembersMap,
): void;
addTypeMember(
isStatic: boolean,
member: TypeMemberImpl
): void;
}
With an InterfaceDeclarationImpl
or a MemberedObjectTypeStructureImpl
, or a TypeMembersMap
, or an ordinary type member, you can define class members to build.
Please note the type members you define might not be the type members you start with. For example, your original interface might say:
interface ColorSpectrum {
colors: string | string[]
}
But you may want to implement:
interface ColorSpectrum {
colors: string[]
}
Here, it’s best to:
MemberedTypeToClass
Later, you can use the original interface as part of an implementsSet
for the class declaration.
declare class MemberedTypeToClass {
/** The class constructor's current parameters list. */
get constructorParameters(): ParameterDeclarationImpl[];
/**
* Add member keys for a field and a group.
* @param isFieldStatic - true if the field is static.
* @param fieldType - the field signature.
* @param isGroupStatic - true if the group is static (false for constructors)
* @param groupType - the group signature, or "constructor" for the constructor I generate.
*/
insertMemberKey(
isFieldStatic: boolean,
fieldType: PropertySignatureImpl,
isGroupStatic: boolean,
groupType: InsertedMemberKey["groupType"]
): void;
}
export interface InsertedMemberKey {
readonly isFieldStatic: boolean;
readonly fieldType: PropertySignatureImpl;
readonly isGroupStatic: boolean;
readonly groupType: (
GetAccessorDeclarationImpl |
SetAccessorDeclarationImpl |
MethodSignatureImpl |
"constructor" |
"(initializer or property reference)" /* ClassFieldStatementsMap.GROUP_INITIALIZER_OR_PROPERTY */
)
}
export interface MemberedStatementsKey {
readonly fieldKey: string;
readonly statementGroupKey: string;
readonly purpose: string;
readonly isFieldStatic: boolean;
readonly fieldType: ReadonlyDeep<TypeMemberImpl> | undefined;
readonly isGroupStatic: boolean;
readonly groupType: ReadonlyDeep<TypeMemberImpl> | undefined;
}
The constructorArguments
are the parameters to define on the class constructor, if one is necessary. (If the constructor has no statements, not even a super()
call, the class members map will omit the constructor.) You can also edit the constructor arguments later, via the constructorParameters
getter.
Sometimes you may need to insert additional statement keys into the set MemberedTypeToClass
visits. (The example I can cite is converting a property to a getter/setter pair in a TypeMembersMap
before the type-to-class code ever sees it. Getters and setters aren’t field keys in the table below, and you may need them to be.) When you run into this, the insertMemberKey()
method exists to provide you the under-the-hood support.
Here is a table of the default keys. You may use insertMemberKey()
to add your own as you need.
Field key | Statement Group Key | Meaning |
---|---|---|
property name | (3) | Initial value for a property |
(static)? getter or setter name |
(3) | A property to mirror |
(1) | (static)? method name or constructor |
Statements leading a statement group |
property name | (static)? method name or constructor |
Statements for the property |
(2) | (static)? method name or constructor |
Statements closing a statement group |
(1) | (static)? get getter name |
Statements leading a statement group |
property name | (static)? get getter name |
Statements for the property |
(2) | (static)? get getter name |
Statements closing a statement group |
(1) | (static)? set setter name |
Statements leading a statement group |
property name | (static)? set setter name |
Statements for the property |
(2) | (static)? set setter name |
Statements closing a statement group |
(1): ClassFieldStatementsMap.FIELD_HEAD_SUPER_CALL
(2): ClassFieldStatementsMap.FIELD_TAIL_FINAL_RETURN
(3): ClassFieldStatementsMap.GROUP_INITIALIZER_OR_PROPERTY
declare class MemberedTypeToClass {
/**
* Add statement getters to this.
*
* @param priority - a number indicating the priority of the getters (lower numbers beat higher numbers).
* @param statementGetters - the statement getters to insert.
*/
addStatementGetters(
priority: number,
statementGetters: readonly ClassStatementsGetter[]
): void;
}
/**
* Traps for getting statements, based on a `MemberedStatementsKey`.
*/
export interface ClassStatementsGetter
extends Partial<PropertyInitializerGetter>, Partial<AccessorMirrorGetter>,
Partial<ClassHeadStatementsGetter>, Partial<ClassBodyStatementsGetter>, Partial<ClassTailStatementsGetter>,
Partial<ConstructorHeadStatementsGetter>, Partial<ConstructorBodyStatementsGetter>, Partial<ConstructorTailStatementsGetter>
{
/** A human-readable string for debugging. */
keyword: readonly string;
/**
* Bitwise flags to determine which statement getter traps are active.
* @see ClassSupportsStatementsFlags
*/
supportsStatementsFlags: readonly number;
}
/**
* Bitwise flags to enable statement getter traps.
*/
export enum ClassSupportsStatementsFlags {
/** The initial value of a property.*/
PropertyInitializer = 1 << 0,
/** Values for a class getter or class setter to mirror. */
AccessorMirror = 1 << 1,
/** Statements starting a statement purpose block. */
HeadStatements = 1 << 2,
/** Statements in a purpose block for a given property and class member. */
BodyStatements = 1 << 3,
/** Statements closing a statement purpose block. */
TailStatements = 1 << 4,
/** Statements starting a statement purpose block for the constructor. */
ConstructorHeadStatements = 1 << 5,
/** Statements in a purpose block for a given property on the constructor. */
ConstructorBodyStatements = 1 << 6,
/** Statements closing a statement purpose block for the constructor. */
ConstructorTailStatements = 1 << 7,
}
This is how we define the actual statements which MemberedTypeToClass
will retrieve:
supportsStatementsFlags
property, based on bitwise operations to enable (and in fact require) callback traps.PropertyInitializerGetter
interface.AccessorMirrorGetter
interface.ClassHeadStatementsGetter
interface for statements leading a purpose blockClassBodyStatementsGetter
interface for statements relating to a class propertyClassTailStatementsGetter
interface for statements closing a purpose blockConstructorHeadStatementsGetter
interface for statements leading a purpose blockConstructorBodyStatementsGetter
interface for statements relating to a class propertyConstructorTailStatementsGetter
interface for statements closing a purpose blockAs for the actual statement filters and getters:
/**
* For the initial value of a property.
*
* @remarks
* To run these methods, `this.supportsStatementsFlags & ClassSupportsStatementsFlags.PropertyInitializer` must be non-zero.
*/
export interface PropertyInitializerGetter {
/**
* @param key - The property description key. `statementGroupKey` will be `ClassFieldStatementsMap.GROUP_INITIALIZER_OR_PROPERTY`.
* @returns true for a match against the key.
*/
filterPropertyInitializer(key: MemberedStatementsKey): boolean;
/**
* @param key - The property description key. `statementGroupKey` will be `ClassFieldStatementsMap.GROUP_INITIALIZER_OR_PROPERTY`.
* @returns the value to write for the property initializer.
*/
getPropertyInitializer(
key: MemberedStatementsKey,
): stringWriterOrStatementImpl | undefined;
}
/**
* A value for getters and setters of a class to reflect.
*
* @remarks
* To run these methods, `this.supportsStatementsFlags & ClassSupportsStatementsFlags.AccessorMirror` must be non-zero.
*/
export interface AccessorMirrorGetter {
/**
* @param key - Describing the getter or setter to implement. `statementGroupKey` will be `ClassFieldStatementsMap.GROUP_INITIALIZER_OR_PROPERTY`.
* @returns true for a match against the key.
*/
filterAccessorMirror(key: MemberedStatementsKey): boolean;
/**
* @param key - Describing the getter or setter to implement. `statementGroupKey` will be `ClassFieldStatementsMap.GROUP_INITIALIZER_OR_PROPERTY`.
* @returns the value to write for the getter and/or setter to mirror.
*/
getAccessorMirror(
key: MemberedStatementsKey,
): stringWriterOrStatementImpl | undefined;
}
/**
* Statements at the start of a statement purpose block.
*
* @remarks
* To run these methods, `this.supportsStatementsFlags & ClassSupportsStatementsFlags.HeadStatements` must be non-zero.
*/
export interface ClassHeadStatementsGetter {
/**
* @param key - The membered statement key. `fieldKey` will be `ClassFieldStatementsMap.FIELD_HEAD_SUPER_CALL`.
* @returns true for a match against the key.
*/
filterHeadStatements(key: MemberedStatementsKey): boolean;
/**
* @param key - The membered statement key. `fieldKey` will be `ClassFieldStatementsMap.FIELD_HEAD_SUPER_CALL`.
* @returns statements to insert before other statements in the purpose block.
*/
getHeadStatements(
key: MemberedStatementsKey,
): readonly stringWriterOrStatementImpl[];
}
/**
* Statements in a statement purpose block for a particular property and function.
*
* @remarks
* To run these methods, `this.supportsStatementsFlags & ClassSupportsStatementsFlags.BodyStatements` must be non-zero.
*/
export interface ClassBodyStatementsGetter {
/**
* @param key - The membered statement key.
* @returns true for a match against the key.
*/
filterBodyStatements(key: MemberedStatementsKey): boolean;
/**
* @param key - The membered statement key.
* @returns statements to insert for the given field key and statement group key.
*/
getBodyStatements(
key: MemberedStatementsKey,
): readonly stringWriterOrStatementImpl[];
}
/**
* Statements at the end of a statement purpose block.
*
* @remarks
* To run these methods, `this.supportsStatementsFlags & ClassSupportsStatementsFlags.TailStatements` must be non-zero.
*/
export interface ClassTailStatementsGetter {
/**
* @param key - The membered statement key. `fieldKey` will be `ClassFieldStatementsMap.FIELD_TAIL_FINAL_RETURN`.
* @returns true for a match against the key.
*/
filterTailStatements(key: MemberedStatementsKey): boolean;
/**
* @param key - The membered statement key. `fieldKey` will be `ClassFieldStatementsMap.FIELD_TAIL_FINAL_RETURN`.
* @returns statements to insert after other statements in the purpose block.
*/
getTailStatements(
key: MemberedStatementsKey,
): readonly stringWriterOrStatementImpl[];
}
/**
* Statements at the start of a constructor's statement purpose block.
*
* @remarks
* To run these methods, `this.supportsStatementsFlags & ClassSupportsStatementsFlags.ConstructorHeadStatements` must be non-zero.
*/
export interface ConstructorHeadStatementsGetter {
/**
* @param key - The membered statement key. `fieldKey` will be `ClassFieldStatementsMap.FIELD_HEAD_SUPER_CALL`. `statementGroupKey` will be "constructor".
* @returns true for a match against the key.
*/
filterCtorHeadStatements(key: MemberedStatementsKey): boolean;
/**
* @param key - The membered statement key. `fieldKey` will be `ClassFieldStatementsMap.FIELD_HEAD_SUPER_CALL`. `statementGroupKey` will be "constructor".
* @returns statements to insert before other statements in the purpose block.
*/
getCtorHeadStatements(
key: MemberedStatementsKey,
): readonly stringWriterOrStatementImpl[];
}
/**
* Statements in a statement purpose block for a particular property in the constructor.
*
* @remarks
* To run these methods, `this.supportsStatementsFlags & ClassSupportsStatementsFlags.ConstructorBodyStatements` must be non-zero.
*/
export interface ConstructorBodyStatementsGetter {
/**
* @param key - The membered statement key. `statementGroupKey` will be "constructor".
* @returns true for a match against the key.
*/
filterCtorBodyStatements(key: MemberedStatementsKey): boolean;
/**
* @param key - The membered statement key. `statementGroupKey` will be "constructor".
* @returns statements to insert for the given field key and statement group key.
*/
getCtorBodyStatements(
key: MemberedStatementsKey,
): readonly stringWriterOrStatementImpl[];
}
/**
* Statements at the end of a constructor's statement purpose block.
*
* @remarks
* To run these methods, `this.supportsStatementsFlags & ClassSupportsStatementsFlags.ConstructorTailStatements` must be non-zero.
*/
export interface ConstructorTailStatementsGetter {
/**
* @param key - The membered statement key. `fieldKey` will be `ClassFieldStatementsMap.FIELD_TAIL_FINAL_RETURN`. `statementGroupKey` will be "constructor".
* @returns true for a match against the key.
*/
filterCtorTailStatements(key: MemberedStatementsKey): boolean;
/**
* @param key - The membered statement key. `fieldKey` will be `ClassFieldStatementsMap.FIELD_TAIL_FINAL_RETURN`. `statementGroupKey` will be "constructor".
* @returns statements to insert before other statements in the purpose block.
*/
getCtorTailStatements(
key: MemberedStatementsKey,
): readonly stringWriterOrStatementImpl[];
}
Mirroring the above table:
Field key | Statement Group Key | Interface |
---|---|---|
property name | (3) | PropertyInitializerGetter |
(static)? getter or setter name |
(3) | AccessorMirrorGetter |
(1) | (static)? method name |
ClassHeadStatementsGetter |
property name | (static)? method name |
ClassBodyStatementsGetter |
(2) | (static)? method name |
ClassTailStatementsGetter |
(1) | (static)? get getter name |
ClassHeadStatementsGetter |
property name | (static)? get getter name |
ClassBodyStatementsGetter |
(2) | (static)? get getter name |
ClassTailStatementsGetter |
(1) | (static)? set setter name |
ClassHeadStatementsGetter |
property name | (static)? set setter name |
ClassBodyStatementsGetter |
(2) | (static)? set setter name |
ClassTailStatementsGetter |
(1) | constructor |
ConstructorHeadStatementsGetter |
property name | constructor |
ConstructorBodyStatementsGetter |
(2) | constructor |
ConstructorTailStatementsGetter |
(1): ClassFieldStatementsMap.FIELD_HEAD_SUPER_CALL
(2): ClassFieldStatementsMap.FIELD_TAIL_FINAL_RETURN
(3): ClassFieldStatementsMap.GROUP_INITIALIZER_OR_PROPERTY
The callbacks each provide useful information to MemberedTypeToClass
. In simplified pseudo-code,
export interface IndexSignatureResolver {
resolveIndexSignature(signature: IndexSignatureDeclarationImpl): string[];
}
export interface ClassAbstractMemberQuestion {
isAbstract(kind: ClassMemberType["kind"], memberName: string): boolean;
}
export interface ClassAsyncMethodQuestion {
isAsync(isStatic: boolean, memberName: string): boolean;
}
export interface ClassGeneratorMethodQuestion {
isGenerator(isStatic: boolean, memberName: string): boolean;
}
export interface ClassScopeMemberQuestion {
getScope(isStatic: boolean, kind: ClassMemberImpl["kind"], memberName: string): Scope | undefined
}
declare class MemberedTypeToClass {
indexSignatureResolver?: IndexSignatureResolver;
isAbstractCallback?: ClassAbstractMemberQuestion;
isAsyncCallback?: ClassAsyncMethodQuestion;
isGeneratorCallback?: ClassGeneratorMethodQuestion;
scopeCallback?: ClassScopeMemberQuestion;
}
declare class MemberedTypeToClass {
defineStatementsByPurpose(
purposeKey: string,
isBlockStatement: boolean,
regionName?: string,
): void;
}
These allow you to define each ClassFieldStatementMap
, in the order you wish the statemeent blocks to appear by purpose.
declare class MemberedTypeToClass {
buildClassMembersMap(): ClassMembersMap;
}
Note this is the final part of the process: after invoking this, none of the other methods of MemberedTypeToClass
should work.
After this, you usually would call .moveMembersToClass(classDecl)
on the ClassMembersMap
.
const
and let
variable declarations.
This is because ts-morph provides no mechanism for organizing class members from a structure. You give ts-morph a ClassDeclarationStructure
, and it decides for you where to put the members. Properties appear in one group, methods in another, getters and setters in another. It’s outside my control.
What you put in, you get out. I provide the ts-morph structures, but I don’t try to format the outputs beyond common sense. Once they go into a ts-morph node, it’s up to you to use utilities like ts-morph or Prettier to clean up the output.
Some time ago, a TypeScript bug on “static implements” inspired me to add Foo satisfies CloneableStructure<FooType>
statements after my classes, to type-check static fields. ClassDeclarationImpl
doesn’t support that (yet), though I could see that being very useful.
You can do dumb things with ts-morph-structures, like provide a getter and a property with the same name. You can do the same dumb things with ts-morph. The utilities here don’t try very hard to protect you from this.
Please, let me know of any pain points you encounter - and suggestions for improving them. Unlike the structure classes, these are more complex. I can add new features as necessary.