diff --git a/src/generators/typescript/TypeScriptGenerator.ts b/src/generators/typescript/TypeScriptGenerator.ts index 33e1f52d20..e7df3e3f2e 100644 --- a/src/generators/typescript/TypeScriptGenerator.ts +++ b/src/generators/typescript/TypeScriptGenerator.ts @@ -36,6 +36,7 @@ import { ClassRenderer } from './renderers/ClassRenderer'; import { InterfaceRenderer } from './renderers/InterfaceRenderer'; import { EnumRenderer } from './renderers/EnumRenderer'; import { TypeRenderer } from './renderers/TypeRenderer'; +import { ConstValueRenderer } from './renderers/ConstValueRenderer'; import { TypeScriptDefaultConstraints, TypeScriptDefaultTypeMapping @@ -301,6 +302,15 @@ ${modelCode}`; ...options }); const dependencyManagerToUse = this.getDependencyManager(optionsToUse); + + // Render const values first (if any exist) + const constValuesOutput = await this.renderConstValue( + model, + inputModel, + optionsToUse + ); + + // Render the class const presets = this.getPresets('class'); const renderer = new ClassRenderer( optionsToUse, @@ -310,7 +320,13 @@ ${modelCode}`; inputModel, dependencyManagerToUse ); - const result = await renderer.runSelfPreset(); + const classResult = await renderer.runSelfPreset(); + + // Combine const values and class + const result = constValuesOutput + ? `${constValuesOutput.result}\n\n${classResult}` + : classResult; + return RenderOutput.toRenderOutput({ result, renderedName: model.name, @@ -318,6 +334,39 @@ ${modelCode}`; }); } + async renderConstValue( + model: ConstrainedObjectModel, + inputModel: InputMetaModel, + options?: DeepPartial + ): Promise { + const optionsToUse = TypeScriptGenerator.getOptions({ + ...this.options, + ...options + }); + const dependencyManagerToUse = this.getDependencyManager(optionsToUse); + const presets = this.getPresets('constValue'); + const renderer = new ConstValueRenderer( + optionsToUse, + this, + presets, + model, + inputModel, + dependencyManagerToUse + ); + + // Check if there are any const properties to render + if (renderer.getConstProperties().length === 0) { + return null; + } + + const result = await renderer.runSelfPreset(); + return RenderOutput.toRenderOutput({ + result, + renderedName: `${model.name}Constants`, + dependencies: dependencyManagerToUse.dependencies + }); + } + async renderInterface( model: ConstrainedObjectModel, inputModel: InputMetaModel, diff --git a/src/generators/typescript/TypeScriptPreset.ts b/src/generators/typescript/TypeScriptPreset.ts index 65d3365a11..6af581a0c1 100644 --- a/src/generators/typescript/TypeScriptPreset.ts +++ b/src/generators/typescript/TypeScriptPreset.ts @@ -4,7 +4,8 @@ import { InterfacePreset, EnumPreset, CommonPreset, - ConstrainedMetaModel + ConstrainedMetaModel, + ConstValuePreset } from '../../models'; import { ClassRenderer, @@ -16,6 +17,10 @@ import { } from './renderers/InterfaceRenderer'; import { EnumRenderer, TS_DEFAULT_ENUM_PRESET } from './renderers/EnumRenderer'; import { TypeRenderer, TS_DEFAULT_TYPE_PRESET } from './renderers/TypeRenderer'; +import { + ConstValueRenderer, + TS_DEFAULT_CONST_VALUE_PRESET +} from './renderers/ConstValueRenderer'; import { TypeScriptOptions } from './TypeScriptGenerator'; export type ClassPresetType = ClassPreset; @@ -26,17 +31,20 @@ export type TypePresetType = CommonPreset< O, ConstrainedMetaModel >; +export type ConstValuePresetType = ConstValuePreset; export type TypeScriptPreset = Preset<{ class: ClassPresetType; interface: InterfacePresetType; enum: EnumPresetType; type: TypePresetType; + constValue: ConstValuePresetType; }>; export const TS_DEFAULT_PRESET: TypeScriptPreset = { class: TS_DEFAULT_CLASS_PRESET, interface: TS_DEFAULT_INTERFACE_PRESET, enum: TS_DEFAULT_ENUM_PRESET, - type: TS_DEFAULT_TYPE_PRESET + type: TS_DEFAULT_TYPE_PRESET, + constValue: TS_DEFAULT_CONST_VALUE_PRESET }; diff --git a/src/generators/typescript/index.ts b/src/generators/typescript/index.ts index 2861978502..226b4ea6ad 100644 --- a/src/generators/typescript/index.ts +++ b/src/generators/typescript/index.ts @@ -3,6 +3,10 @@ export * from './TypeScriptFileGenerator'; export { TS_DEFAULT_PRESET } from './TypeScriptPreset'; export type { TypeScriptPreset } from './TypeScriptPreset'; export * from './presets'; +export { + ConstValueRenderer, + TS_DEFAULT_CONST_VALUE_PRESET +} from './renderers/ConstValueRenderer'; export { defaultEnumKeyConstraints as typeScriptDefaultEnumKeyConstraints, diff --git a/src/generators/typescript/renderers/ClassRenderer.ts b/src/generators/typescript/renderers/ClassRenderer.ts index 80c55af2de..d876bafeda 100644 --- a/src/generators/typescript/renderers/ClassRenderer.ts +++ b/src/generators/typescript/renderers/ClassRenderer.ts @@ -63,7 +63,7 @@ export const TS_DEFAULT_CLASS_PRESET: ClassPresetType = { continue; } assignments.push(`this._${propertyName} = input.${propertyName};`); - ctorProperties.push(renderer.renderProperty(property).replace(';', ',')); + ctorProperties.push(renderer.renderProperty(property).replaceAll(';', ',')); } return `constructor(input: { @@ -76,13 +76,18 @@ ${renderer.indent(renderer.renderBlock(assignments))} return `private _${renderer.renderProperty(property)}`; }, getter({ property }): string { - return `get ${property.propertyName}(): ${ - property.property.options.const?.value - ? property.property.options.const.value - : property.property.type - }${property.required === false ? ' | undefined' : ''} { return this._${ - property.propertyName - }; }`; + const constVal = property.property.options.const?.value; + // Use JSON.stringify for non-string values to avoid [object Object] issues + let returnType: string; + if (constVal === undefined) { + returnType = property.property.type; + } else if (typeof constVal === 'string') { + returnType = constVal; + } else { + returnType = JSON.stringify(constVal); + } + const optionalSuffix = property.required === false ? ' | undefined' : ''; + return `get ${property.propertyName}(): ${returnType}${optionalSuffix} { return this._${property.propertyName}; }`; }, setter({ property }): string { // if const value exists we should not render a setter @@ -90,10 +95,8 @@ ${renderer.indent(renderer.renderBlock(assignments))} return ''; } - return `set ${property.propertyName}(${property.propertyName}: ${ - property.property.type - }${property.required === false ? ' | undefined' : ''}) { this._${ - property.propertyName - } = ${property.propertyName}; }`; + return `set ${property.propertyName}(${property.propertyName}: ${property.property.type + }${property.required === false ? ' | undefined' : ''}) { this._${property.propertyName + } = ${property.propertyName}; }`; } }; diff --git a/src/generators/typescript/renderers/ConstValueRenderer.ts b/src/generators/typescript/renderers/ConstValueRenderer.ts new file mode 100644 index 0000000000..7dedc22726 --- /dev/null +++ b/src/generators/typescript/renderers/ConstValueRenderer.ts @@ -0,0 +1,80 @@ +import { TypeScriptRenderer } from '../TypeScriptRenderer'; +import { + ConstrainedObjectModel, + ConstrainedObjectPropertyModel +} from '../../../models'; +import { ConstValuePresetType } from '../TypeScriptPreset'; +import { TypeScriptOptions } from '../TypeScriptGenerator'; + +/** + * Renderer for TypeScript's exported const values + * + * This renderer generates exported const declarations for properties + * that have const values defined in the schema. + * + * @extends TypeScriptRenderer + */ +export class ConstValueRenderer extends TypeScriptRenderer { + async defaultSelf(): Promise { + const content = [ + await this.renderItems(), + await this.runAdditionalContentPreset() + ]; + + return this.renderBlock(content); + } + + /** + * Get all properties that have const values defined + */ + getConstProperties(): ConstrainedObjectPropertyModel[] { + return Object.values(this.model.properties).filter( + (prop) => prop.property.options.const?.value !== undefined + ); + } + + /** + * Converts a property name from camelCase to UPPER_SNAKE_CASE + */ + toConstName(propertyName: string): string { + return propertyName.replaceAll(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(); + } + + async renderItems(): Promise { + const constProperties = this.getConstProperties(); + const items: string[] = []; + + for (const property of constProperties) { + const renderedItem = await this.runItemPreset(property); + if (renderedItem) { + items.push(renderedItem); + } + } + + return this.renderBlock(items); + } + + runItemPreset(property: ConstrainedObjectPropertyModel): Promise { + return this.runPreset('item', { property }); + } +} + +export const TS_DEFAULT_CONST_VALUE_PRESET: ConstValuePresetType = + { + self({ renderer }) { + return renderer.defaultSelf(); + }, + item({ property, renderer }): string { + const constValue = property.property.options.const?.value; + if (constValue === undefined) { + return ''; + } + + const constName = renderer.toConstName(property.propertyName); + // Use proper formatting based on value type + const formattedValue = + typeof constValue === 'string' ? constValue : JSON.stringify(constValue); + + return `export const ${constName} = ${formattedValue};`; + } + }; diff --git a/src/models/Preset.ts b/src/models/Preset.ts index d8d69ef6d9..6c10227981 100644 --- a/src/models/Preset.ts +++ b/src/models/Preset.ts @@ -71,6 +71,13 @@ export interface EnumPreset item?: (args: PresetArgs & EnumArgs) => string; } +export interface ConstValuePreset + extends CommonPreset { + item?: ( + args: PresetArgs & PropertyArgs + ) => string; +} + export type Preset< C extends Record> = any > = Partial; diff --git a/test/generators/typescript/ConstValueRenderer.spec.ts b/test/generators/typescript/ConstValueRenderer.spec.ts new file mode 100644 index 0000000000..201735ed31 --- /dev/null +++ b/test/generators/typescript/ConstValueRenderer.spec.ts @@ -0,0 +1,219 @@ +import { + ConstrainedObjectModel, + ConstrainedObjectPropertyModel, + ConstrainedStringModel, + InputMetaModel +} from '../../../src'; +import { TypeScriptGenerator } from '../../../src/generators'; +import { + ConstValueRenderer, + TS_DEFAULT_CONST_VALUE_PRESET +} from '../../../src/generators/typescript/renderers/ConstValueRenderer'; +import { TypeScriptDependencyManager } from '../../../src/generators/typescript/TypeScriptDependencyManager'; + +describe('ConstValueRenderer', () => { + let renderer: ConstValueRenderer; + let generator: TypeScriptGenerator; + let dependencyManager: TypeScriptDependencyManager; + + const createModelWithConstProperties = ( + properties: Record< + string, + { type: string; constValue?: string | number | boolean } + > + ): ConstrainedObjectModel => { + const modelProperties: Record = {}; + + for (const [name, prop] of Object.entries(properties)) { + const propertyModel = new ConstrainedStringModel(name, undefined, {}, prop.type); + if (prop.constValue !== undefined) { + propertyModel.options.const = { + originalInput: prop.constValue, + value: prop.constValue + }; + } + + modelProperties[name] = new ConstrainedObjectPropertyModel( + name, + name, + true, + propertyModel + ); + } + + return new ConstrainedObjectModel('TestModel', undefined, {}, 'object', modelProperties); + }; + + beforeEach(() => { + generator = new TypeScriptGenerator(); + dependencyManager = new TypeScriptDependencyManager( + TypeScriptGenerator.defaultOptions + ); + }); + + describe('getConstProperties()', () => { + test('should return properties with const values', () => { + const model = createModelWithConstProperties({ + eventType: { type: 'string', constValue: 'EXAMPLE_EVENT' }, + normalProp: { type: 'string' } + }); + + renderer = new ConstValueRenderer( + TypeScriptGenerator.defaultOptions, + generator, + [[TS_DEFAULT_CONST_VALUE_PRESET, {}]], + model, + new InputMetaModel(), + dependencyManager + ); + + const constProps = renderer.getConstProperties(); + expect(constProps).toHaveLength(1); + expect(constProps[0].propertyName).toBe('eventType'); + }); + + test('should return empty array when no const properties exist', () => { + const model = createModelWithConstProperties({ + normalProp1: { type: 'string' }, + normalProp2: { type: 'number' } + }); + + renderer = new ConstValueRenderer( + TypeScriptGenerator.defaultOptions, + generator, + [[TS_DEFAULT_CONST_VALUE_PRESET, {}]], + model, + new InputMetaModel(), + dependencyManager + ); + + const constProps = renderer.getConstProperties(); + expect(constProps).toHaveLength(0); + }); + }); + + describe('toConstName()', () => { + beforeEach(() => { + const model = createModelWithConstProperties({}); + renderer = new ConstValueRenderer( + TypeScriptGenerator.defaultOptions, + generator, + [[TS_DEFAULT_CONST_VALUE_PRESET, {}]], + model, + new InputMetaModel(), + dependencyManager + ); + }); + + test('should convert camelCase to UPPER_SNAKE_CASE', () => { + expect(renderer.toConstName('eventType')).toBe('EVENT_TYPE'); + expect(renderer.toConstName('myPropertyName')).toBe('MY_PROPERTY_NAME'); + expect(renderer.toConstName('petType')).toBe('PET_TYPE'); + }); + + test('should handle single word names', () => { + expect(renderer.toConstName('name')).toBe('NAME'); + expect(renderer.toConstName('type')).toBe('TYPE'); + }); + + test('should handle already uppercase names', () => { + expect(renderer.toConstName('TYPE')).toBe('TYPE'); + }); + }); + + describe('defaultSelf()', () => { + test('should render exported constants for const properties', async () => { + const model = createModelWithConstProperties({ + eventType: { type: 'string', constValue: 'EXAMPLE_EVENT' }, + eventStatus: { type: 'string', constValue: 'ACTIVE' } + }); + + renderer = new ConstValueRenderer( + TypeScriptGenerator.defaultOptions, + generator, + [[TS_DEFAULT_CONST_VALUE_PRESET, {}]], + model, + new InputMetaModel(), + dependencyManager + ); + + const result = await renderer.defaultSelf(); + expect(result).toContain("export const EVENT_TYPE = EXAMPLE_EVENT;"); + expect(result).toContain("export const EVENT_STATUS = ACTIVE;"); + }); + + test('should return empty string when no const properties exist', async () => { + const model = createModelWithConstProperties({ + normalProp: { type: 'string' } + }); + + renderer = new ConstValueRenderer( + TypeScriptGenerator.defaultOptions, + generator, + [[TS_DEFAULT_CONST_VALUE_PRESET, {}]], + model, + new InputMetaModel(), + dependencyManager + ); + + const result = await renderer.defaultSelf(); + expect(result.trim()).toBe(''); + }); + }); + + describe('TS_DEFAULT_CONST_VALUE_PRESET', () => { + test('item preset should format string values correctly', () => { + const model = createModelWithConstProperties({ + eventType: { type: 'string', constValue: 'EXAMPLE_EVENT' } + }); + + renderer = new ConstValueRenderer( + TypeScriptGenerator.defaultOptions, + generator, + [[TS_DEFAULT_CONST_VALUE_PRESET, {}]], + model, + new InputMetaModel(), + dependencyManager + ); + + const property = Object.values(model.properties)[0]; + const result = TS_DEFAULT_CONST_VALUE_PRESET.item!({ + property, + renderer, + model, + inputModel: new InputMetaModel(), + options: TypeScriptGenerator.defaultOptions, + content: '' + }); + + expect(result).toBe("export const EVENT_TYPE = EXAMPLE_EVENT;"); + }); + + test('item preset should return empty string for non-const properties', () => { + const model = createModelWithConstProperties({ + normalProp: { type: 'string' } + }); + + renderer = new ConstValueRenderer( + TypeScriptGenerator.defaultOptions, + generator, + [[TS_DEFAULT_CONST_VALUE_PRESET, {}]], + model, + new InputMetaModel(), + dependencyManager + ); + + const property = Object.values(model.properties)[0]; + const result = TS_DEFAULT_CONST_VALUE_PRESET.item!({ + property, + renderer, + model, + inputModel: new InputMetaModel(), + options: TypeScriptGenerator.defaultOptions, + content: '' + }); + + expect(result).toBe(''); + }); + }); +}); diff --git a/test/generators/typescript/TypeScriptGenerator.spec.ts b/test/generators/typescript/TypeScriptGenerator.spec.ts index 085501ec40..3cfb2674ee 100644 --- a/test/generators/typescript/TypeScriptGenerator.spec.ts +++ b/test/generators/typescript/TypeScriptGenerator.spec.ts @@ -987,5 +987,69 @@ ${content}`; }); expect(models.map((model) => model.result)).toMatchSnapshot(); }); + + test('should generate exported constants for const properties', async () => { + const models = await generator.generate({ + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + title: 'EventMessage', + additionalProperties: false, + properties: { + eventType: { + type: 'string', + const: 'EXAMPLE_EVENT' + }, + eventStatus: { + type: 'string', + const: 'ACTIVE' + }, + name: { + type: 'string' + } + } + }); + expect(models).toHaveLength(1); + const result = models[0].result; + + // Should contain exported constants with UPPER_SNAKE_CASE names + expect(result).toContain("export const EVENT_TYPE = 'EXAMPLE_EVENT';"); + expect(result).toContain("export const EVENT_STATUS = 'ACTIVE';"); + + // Constants should appear before the class definition + const constIndex = result.indexOf('export const'); + const classIndex = result.indexOf('class EventMessage'); + expect(constIndex).toBeLessThan(classIndex); + + // Non-const property should not generate an exported constant + expect(result).not.toContain('export const NAME'); + expect(result).not.toContain('export const RESERVED_NAME'); + + expect(result).toMatchSnapshot(); + }); + + test('should not generate exported constants when no const properties exist', async () => { + const models = await generator.generate({ + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + title: 'SimpleMessage', + additionalProperties: false, + properties: { + name: { + type: 'string' + }, + age: { + type: 'number' + } + } + }); + expect(models).toHaveLength(1); + const result = models[0].result; + + // Should not contain any exported constants + expect(result).not.toContain('export const'); + + // Should still render the class normally + expect(result).toContain('class SimpleMessage'); + }); }); }); diff --git a/test/generators/typescript/__snapshots__/TypeScriptGenerator.spec.ts.snap b/test/generators/typescript/__snapshots__/TypeScriptGenerator.spec.ts.snap index cdacd89e0c..a9f335f1d0 100644 --- a/test/generators/typescript/__snapshots__/TypeScriptGenerator.spec.ts.snap +++ b/test/generators/typescript/__snapshots__/TypeScriptGenerator.spec.ts.snap @@ -3,7 +3,9 @@ exports[`TypeScriptGenerator AsyncAPI with polymorphism should render 6 models (1 oneOf, 3 classes and 2 enums) 1`] = ` Array [ "type AnonymousSchema_1 = Cat | Dog | StickInsect;", - "class Cat { + "export const PET_TYPE = PetType.CAT; + +class Cat { private _petType: PetType.CAT = PetType.CAT; private _reservedName: string; private _huntingSkill: HuntingSkill; @@ -35,7 +37,9 @@ Array [ ADVENTUROUS = \\"adventurous\\", AGGRESSIVE = \\"aggressive\\", }", - "class Dog { + "export const PET_TYPE = PetType.DOG; + +class Dog { private _petType: PetType.DOG = PetType.DOG; private _reservedName: string; private _packSize: number; @@ -56,7 +60,9 @@ Array [ get packSize(): number { return this._packSize; } set packSize(packSize: number) { this._packSize = packSize; } }", - "class StickInsect { + "export const PET_TYPE = PetType.STICK_BUG; + +class StickInsect { private _petType: PetType.STICK_BUG = PetType.STICK_BUG; private _reservedName: string; private _color: string; @@ -91,7 +97,9 @@ exports[`TypeScriptGenerator AsyncAPI with polymorphism should render enum with exports[`TypeScriptGenerator CloudEvent handle allOf with const in CloudEvent type 1`] = ` Array [ "type Pet = Dog | Cat;", - "class Dog { + "export const RESERVED_TYPE = DogType.DOG; + +class Dog { private _id: string; private _source: string; private _specversion: string; @@ -139,7 +147,9 @@ Array [ "enum DogType { DOG = \\"Dog\\", }", - "class Cat { + "export const RESERVED_TYPE = CatType.CAT; + +class Cat { private _id: string; private _source: string; private _specversion: string; @@ -193,7 +203,9 @@ Array [ exports[`TypeScriptGenerator Combine oneOf and allOf should combine oneOf and allOf 1`] = ` Array [ "type Pet = Cat | Dog;", - "class Cat { + "export const ANIMAL_TYPE = AnimalType.CAT; + +class Cat { private _animalType?: AnimalType.CAT = AnimalType.CAT; private _age?: number; private _huntingSkill?: HuntingSkill; @@ -222,7 +234,9 @@ Array [ CLUELESS = \\"clueless\\", LAZY = \\"lazy\\", }", - "class Dog { + "export const ANIMAL_TYPE = AnimalType.DOG; + +class Dog { private _animalType?: AnimalType.DOG = AnimalType.DOG; private _age?: number; private _breed?: DogBreed; @@ -253,7 +267,9 @@ Array [ exports[`TypeScriptGenerator Combine properties and oneOf should combine properties and oneOf 1`] = ` Array [ "type Pet = Cat | Dog;", - "class Cat { + "export const PET_TYPE = PetType.CAT; + +class Cat { private _petType: PetType.CAT = PetType.CAT; private _age?: number; private _huntingSkill?: HuntingSkill; @@ -282,7 +298,9 @@ Array [ CLUELESS = \\"clueless\\", LAZY = \\"lazy\\", }", - "class Dog { + "export const PET_TYPE = PetType.DOG; + +class Dog { private _petType: PetType.DOG = PetType.DOG; private _age?: number; private _breed?: DogBreed; @@ -312,7 +330,9 @@ Array [ exports[`TypeScriptGenerator const should generate a const string 1`] = ` Array [ - "class LightMeasured { + "export const RESERVED_TYPE = 'test'; + +class LightMeasured { private _reservedType?: 'test' = 'test'; constructor(input: { @@ -328,7 +348,10 @@ Array [ exports[`TypeScriptGenerator const should generate a single enum with two values 1`] = ` Array [ - "class LightMeasured { + "export const RESERVED_TYPE = ReservedType.MY_MESSAGE; +export const TYPE2 = Type2.MY_MESSAGE2; + +class LightMeasured { private _reservedType?: ReservedType.MY_MESSAGE = ReservedType.MY_MESSAGE; private _type2?: Type2.MY_MESSAGE2 = Type2.MY_MESSAGE2; @@ -374,6 +397,30 @@ Array [ ] `; +exports[`TypeScriptGenerator const should generate exported constants for const properties 1`] = ` +"export const EVENT_TYPE = 'EXAMPLE_EVENT'; +export const EVENT_STATUS = 'ACTIVE'; + +class EventMessage { + private _eventType?: 'EXAMPLE_EVENT' = 'EXAMPLE_EVENT'; + private _eventStatus?: 'ACTIVE' = 'ACTIVE'; + private _reservedName?: string; + + constructor(input: { + reservedName?: string, + }) { + this._reservedName = input.reservedName; + } + + get eventType(): 'EXAMPLE_EVENT' | undefined { return this._eventType; } + + get eventStatus(): 'ACTIVE' | undefined { return this._eventStatus; } + + get reservedName(): string | undefined { return this._reservedName; } + set reservedName(reservedName: string | undefined) { this._reservedName = reservedName; } +}" +`; + exports[`TypeScriptGenerator if/then/else handle if/then/else required properties 1`] = ` Array [ "class ReservedEvent { diff --git a/test/generators/typescript/preset/__snapshots__/MarshallingPreset.spec.ts.snap b/test/generators/typescript/preset/__snapshots__/MarshallingPreset.spec.ts.snap index 9edea5549e..056ec88e3f 100644 --- a/test/generators/typescript/preset/__snapshots__/MarshallingPreset.spec.ts.snap +++ b/test/generators/typescript/preset/__snapshots__/MarshallingPreset.spec.ts.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Marshalling preset should render un/marshal code 1`] = ` -"class Test { +"export const CONST_TEST = 'TEST'; + +class Test { private _stringProp: string; private _enumProp?: EnumTest; private _numberProp?: number;