From 609385f3f380b733f7d8d0a35086bad49f6be830 Mon Sep 17 00:00:00 2001 From: Lorenzo Dal'Aqua Date: Fri, 31 Jul 2026 15:28:03 -0300 Subject: [PATCH 1/6] Support plain TypeScript enums in WriteEnumConst const enum members are inlined by the TypeScript compiler and produce no runtime object, so consumers can't read enum values at runtime (e.g. Object.values()). Add a plainEnums parameter, defaulting to false, so the writer can emit a plain export enum instead when runtime access is needed. --- src/Actions/WriteEnumConst.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Actions/WriteEnumConst.php b/src/Actions/WriteEnumConst.php index f2eacb1..16476d6 100644 --- a/src/Actions/WriteEnumConst.php +++ b/src/Actions/WriteEnumConst.php @@ -11,7 +11,7 @@ class WriteEnumConst * * @return array{type: string, name: string}|string */ - public function __invoke(ReflectionClass $reflection, string $indent = '', bool $jsonOutput = false, bool $useEnums = false): array|string + public function __invoke(ReflectionClass $reflection, string $indent = '', bool $jsonOutput = false, bool $useEnums = false, bool $plainEnums = false): array|string { $entry = ''; @@ -30,7 +30,8 @@ public function __invoke(ReflectionClass $reflection, string $indent = '', bool if ($cases->isNotEmpty()) { if ($useEnums) { - $entry .= "{$indent}export const enum {$reflection->getShortName()} {" . PHP_EOL; + $enumKeyword = $plainEnums ? 'enum' : 'const enum'; + $entry .= "{$indent}export {$enumKeyword} {$reflection->getShortName()} {" . PHP_EOL; } else { $entry .= "{$indent}const {$reflection->getShortName()} = {" . PHP_EOL; } From d882ca80b3ef78fd3b242b92e1dbe7572a54c37e Mon Sep 17 00:00:00 2001 From: Lorenzo Dal'Aqua Date: Fri, 31 Jul 2026 15:29:43 -0300 Subject: [PATCH 2/6] Thread plainEnums through CLI and JSON output generators WriteEnumConst's new plainEnums parameter needs a path from the command entrypoint down to where enums are written. Wire it through Generator, GenerateCliOutput, and GenerateJsonOutput the same way useEnums already reaches both output paths. --- src/Actions/GenerateCliOutput.php | 6 +++--- src/Actions/GenerateJsonOutput.php | 6 +++--- src/Actions/Generator.php | 8 +++++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Actions/GenerateCliOutput.php b/src/Actions/GenerateCliOutput.php index c5d4f73..064c3e3 100644 --- a/src/Actions/GenerateCliOutput.php +++ b/src/Actions/GenerateCliOutput.php @@ -37,7 +37,7 @@ class GenerateCliOutput * * @throws \ReflectionException */ - public function __invoke(Collection $models, array $mappings, bool $global = false, bool $useEnums = false, bool $useTypes = false, bool $plurals = false, bool $apiResources = false, bool $optionalRelations = false, bool $noRelations = false, bool $noHidden = false, bool $noCounts = false, bool $optionalCounts = false, bool $noExists = false, bool $optionalExists = false, bool $noSums = false, bool $optionalSums = false, bool $optionalNullables = false, bool $fillables = false, string $fillableSuffix = 'Fillable'): string + public function __invoke(Collection $models, array $mappings, bool $global = false, bool $useEnums = false, bool $plainEnums = false, bool $useTypes = false, bool $plurals = false, bool $apiResources = false, bool $optionalRelations = false, bool $noRelations = false, bool $noHidden = false, bool $noCounts = false, bool $optionalCounts = false, bool $noExists = false, bool $optionalExists = false, bool $noSums = false, bool $optionalSums = false, bool $optionalNullables = false, bool $fillables = false, string $fillableSuffix = 'Fillable'): string { $modelBuilder = app(BuildModelDetails::class); $colAttrWriter = app(WriteColumnAttribute::class); @@ -181,8 +181,8 @@ public function __invoke(Collection $models, array $mappings, bool $global = fal collect($this->enumReflectors) ->unique(fn (ReflectionClass $reflector) => $reflector->getName()) - ->each(function (ReflectionClass $reflector) use ($useEnums) { - $this->output .= app(WriteEnumConst::class)($reflector, $this->indent, false, $useEnums); + ->each(function (ReflectionClass $reflector) use ($useEnums, $plainEnums) { + $this->output .= app(WriteEnumConst::class)($reflector, $this->indent, false, $useEnums, $plainEnums); }); collect($this->imports) diff --git a/src/Actions/GenerateJsonOutput.php b/src/Actions/GenerateJsonOutput.php index a82ef9f..a69104d 100644 --- a/src/Actions/GenerateJsonOutput.php +++ b/src/Actions/GenerateJsonOutput.php @@ -35,7 +35,7 @@ class GenerateJsonOutput * * @throws ReflectionException */ - public function __invoke(Collection $models, array $mappings, bool $useEnums = false, bool $noCounts = false, bool $optionalCounts = false, bool $noExists = false, bool $optionalExists = false, bool $noSums = false, bool $optionalSums = false): string + public function __invoke(Collection $models, array $mappings, bool $useEnums = false, bool $plainEnums = false, bool $noCounts = false, bool $optionalCounts = false, bool $noExists = false, bool $optionalExists = false, bool $noSums = false, bool $optionalSums = false): string { $modelBuilder = app(BuildModelDetails::class); $colAttrWriter = app(WriteColumnAttribute::class); @@ -119,8 +119,8 @@ public function __invoke(Collection $models, array $mappings, bool $useEnums = f })->toArray(); }); - $this->output['enums'] = collect($this->enumReflectors)->map(function ($enum) use ($enumWriter, $useEnums) { - $enumConst = $enumWriter(reflection: $enum, jsonOutput: true, useEnums: $useEnums); + $this->output['enums'] = collect($this->enumReflectors)->map(function ($enum) use ($enumWriter, $useEnums, $plainEnums) { + $enumConst = $enumWriter(reflection: $enum, jsonOutput: true, useEnums: $useEnums, plainEnums: $plainEnums); return [ $enumConst['name'] => [ diff --git a/src/Actions/Generator.php b/src/Actions/Generator.php index b2ad6eb..968b0d1 100644 --- a/src/Actions/Generator.php +++ b/src/Actions/Generator.php @@ -19,7 +19,7 @@ class Generator * @throws ModelTyperException * @throws ReflectionException */ - public function __invoke(?string $specificModel = null, bool $global = false, bool $json = false, bool $useEnums = false, bool $useTypes = false, bool $plurals = false, bool $apiResources = false, bool $optionalRelations = false, bool $noRelations = false, bool $noHidden = false, bool $noCounts = false, bool $optionalCounts = false, bool $noExists = false, bool $optionalExists = false, bool $noSums = false, bool $optionalSums = false, bool $timestampsDate = false, bool $optionalNullables = false, bool $fillables = false, string $fillableSuffix = 'Fillable'): string + public function __invoke(?string $specificModel = null, bool $global = false, bool $json = false, bool $useEnums = false, bool $plainEnums = false, bool $useTypes = false, bool $plurals = false, bool $apiResources = false, bool $optionalRelations = false, bool $noRelations = false, bool $noHidden = false, bool $noCounts = false, bool $optionalCounts = false, bool $noExists = false, bool $optionalExists = false, bool $noSums = false, bool $optionalSums = false, bool $timestampsDate = false, bool $optionalNullables = false, bool $fillables = false, string $fillableSuffix = 'Fillable'): string { $models = app(GetModels::class)( model: $specificModel, @@ -37,6 +37,7 @@ public function __invoke(?string $specificModel = null, bool $global = false, bo global: $global, json: $json, useEnums: $useEnums, + plainEnums: $plainEnums, useTypes: $useTypes, plurals: $plurals, apiResources: $apiResources, @@ -63,12 +64,12 @@ public function __invoke(?string $specificModel = null, bool $global = false, bo * * @throws ReflectionException */ - protected function display(Collection $models, bool $global = false, bool $json = false, bool $useEnums = false, bool $useTypes = false, bool $plurals = false, bool $apiResources = false, bool $optionalRelations = false, bool $noRelations = false, bool $noHidden = false, bool $noCounts = false, bool $optionalCounts = false, bool $noExists = false, bool $optionalExists = false, bool $noSums = false, bool $optionalSums = false, bool $timestampsDate = false, bool $optionalNullables = false, bool $fillables = false, string $fillableSuffix = 'Fillable'): string + protected function display(Collection $models, bool $global = false, bool $json = false, bool $useEnums = false, bool $plainEnums = false, bool $useTypes = false, bool $plurals = false, bool $apiResources = false, bool $optionalRelations = false, bool $noRelations = false, bool $noHidden = false, bool $noCounts = false, bool $optionalCounts = false, bool $noExists = false, bool $optionalExists = false, bool $noSums = false, bool $optionalSums = false, bool $timestampsDate = false, bool $optionalNullables = false, bool $fillables = false, string $fillableSuffix = 'Fillable'): string { $mappings = app(GetMappings::class)(setTimestampsToDate: $timestampsDate); if ($json) { - return app(GenerateJsonOutput::class)(models: $models, mappings: $mappings, useEnums: $useEnums, noCounts: $noCounts, optionalCounts: $optionalCounts, noExists: $noExists, optionalExists: $optionalExists, noSums: $noSums, optionalSums: $optionalSums); + return app(GenerateJsonOutput::class)(models: $models, mappings: $mappings, useEnums: $useEnums, plainEnums: $plainEnums, noCounts: $noCounts, optionalCounts: $optionalCounts, noExists: $noExists, optionalExists: $optionalExists, noSums: $noSums, optionalSums: $optionalSums); } return app(GenerateCliOutput::class)( @@ -76,6 +77,7 @@ protected function display(Collection $models, bool $global = false, bool $json mappings: $mappings, global: $global, useEnums: $useEnums, + plainEnums: $plainEnums, useTypes: $useTypes, plurals: $plurals, apiResources: $apiResources, From 7ffdb2253c226e2a787bf336ebb5d20359a5954f Mon Sep 17 00:00:00 2001 From: Lorenzo Dal'Aqua Date: Fri, 31 Jul 2026 15:31:23 -0300 Subject: [PATCH 3/6] Add --plain-enums CLI flag and config option Exposes the plainEnums behavior through the same config-or-CLI pattern as every other option. I considered making use-enums itself a three-way value (object, const, plain) instead of adding a second boolean, since that would make the one meaningless combination (use-enums off, plain-enums on) unrepresentable. I kept use-enums as a plain boolean and added a modifier instead, because it's an existing public flag and every other option in this package is a flat boolean; changing its type would mean true/false permanently carry legacy meaning while new string values mean something else. I defaulted plain-enums to false rather than adding a const-enums flag defaulting to true, because every boolean flag here is a presence-only Artisan option with no --no-flag negation. A default-true flag could be turned on redundantly from the CLI but never off, forcing anyone who wants plain enums to edit the published config file instead of just passing a flag. --- config/modeltyper.php | 14 ++++++++++++++ src/Commands/ModelTyperCommand.php | 2 ++ 2 files changed, 16 insertions(+) diff --git a/config/modeltyper.php b/config/modeltyper.php index 5da0a25..45d1060 100644 --- a/config/modeltyper.php +++ b/config/modeltyper.php @@ -82,6 +82,20 @@ */ 'use-enums' => false, + /* + |-------------------------------------------------------------------------- + | Emit Plain TypeScript Enums Instead of Const Enums + |-------------------------------------------------------------------------- + | + | Determines whether enums emitted by use-enums use the plain "enum" syntax + | instead of "const enum". Const enum members are inlined by the TypeScript + | compiler and are not accessible at runtime; plain enums produce a real + | object, so their values can be read at runtime. + | + | Requires use-enums set to true + */ + 'plain-enums' => false, + /* |-------------------------------------------------------------------------- | Use TypeScript Types Instead of Interfaces diff --git a/src/Commands/ModelTyperCommand.php b/src/Commands/ModelTyperCommand.php index 3a7e458..ca4784b 100644 --- a/src/Commands/ModelTyperCommand.php +++ b/src/Commands/ModelTyperCommand.php @@ -32,6 +32,7 @@ class ModelTyperCommand extends Command {--global : Generate typescript interfaces in a global namespace named models} {--json : Output the result as json} {--use-enums : Use typescript enums instead of object literals} + {--plain-enums : Emit a plain typescript enum instead of a const enum when --use-enums is set, so values are readable at runtime} {--use-types : Use typescript types instead of interfaces} {--plurals : Output model plurals} {--no-relations : Do not include relations} @@ -79,6 +80,7 @@ public function handle(Generator $generator): int global: $this->getConfig('global'), json: $this->getConfig('json'), useEnums: $this->getConfig('use-enums'), + plainEnums: $this->getConfig('plain-enums'), useTypes: $this->getConfig('use-types'), plurals: $this->getConfig('plurals'), apiResources: $this->getConfig('api-resources'), From 4a7ef9c27ef179edd9446c28dc95adbbec6ac366 Mon Sep 17 00:00:00 2001 From: Lorenzo Dal'Aqua Date: Fri, 31 Jul 2026 15:32:53 -0300 Subject: [PATCH 4/6] Cover const vs plain enum emission in WriteEnumConst Adds direct coverage for both branches of the useEnums=true path, alongside a new test for plainEnums. The const enum branch was previously only exercised indirectly through the full command, so this pins its output at the unit level too. --- .../Feature/Actions/WriteEnumConstTest.php | 26 +++++++++++++++++++ test/input/expectations/enum-const.ts | 10 +++++++ test/input/expectations/enum-plain.ts | 10 +++++++ 3 files changed, 46 insertions(+) create mode 100644 test/input/expectations/enum-const.ts create mode 100644 test/input/expectations/enum-plain.ts diff --git a/test/Tests/Feature/Actions/WriteEnumConstTest.php b/test/Tests/Feature/Actions/WriteEnumConstTest.php index 0d83c93..be87448 100644 --- a/test/Tests/Feature/Actions/WriteEnumConstTest.php +++ b/test/Tests/Feature/Actions/WriteEnumConstTest.php @@ -44,4 +44,30 @@ public function test_action_can_be_executed_and_returns_array() $this->assertEquals('Roles', $result['name']); $this->assertIsString($result['type']); } + + public function test_action_generates_const_enum_when_use_enums_is_enabled() + { + $action = app(WriteEnumConst::class); + $reflectionModel = $this->resolveClassAsReflection(Roles::class); + + $result = $action(reflection: $reflectionModel, useEnums: true); + + $expected = $this->getExpectedContent('enum-const.ts', true); + + $this->assertIsString($result); + $this->assertEquals($expected, $result); + } + + public function test_action_generates_plain_enum_when_plain_enums_is_enabled() + { + $action = app(WriteEnumConst::class); + $reflectionModel = $this->resolveClassAsReflection(Roles::class); + + $result = $action(reflection: $reflectionModel, useEnums: true, plainEnums: true); + + $expected = $this->getExpectedContent('enum-plain.ts', true); + + $this->assertIsString($result); + $this->assertEquals($expected, $result); + } } diff --git a/test/input/expectations/enum-const.ts b/test/input/expectations/enum-const.ts new file mode 100644 index 0000000..4bbe89d --- /dev/null +++ b/test/input/expectations/enum-const.ts @@ -0,0 +1,10 @@ +export const enum Roles { + /** Can do anything */ + ADMIN = 'admin', + /** Standard readonly */ + USER = 'user', + /** Value that needs string escaping */ + USERCLASS = 'App\\Models\\User', +} + +export type RolesEnum = `${Roles}` diff --git a/test/input/expectations/enum-plain.ts b/test/input/expectations/enum-plain.ts new file mode 100644 index 0000000..6f444a8 --- /dev/null +++ b/test/input/expectations/enum-plain.ts @@ -0,0 +1,10 @@ +export enum Roles { + /** Can do anything */ + ADMIN = 'admin', + /** Standard readonly */ + USER = 'user', + /** Value that needs string escaping */ + USERCLASS = 'App\\Models\\User', +} + +export type RolesEnum = `${Roles}` From ed7022f42a1778ca17481f7b5676eb104b91edc7 Mon Sep 17 00:00:00 2001 From: Lorenzo Dal'Aqua Date: Fri, 31 Jul 2026 15:33:37 -0300 Subject: [PATCH 5/6] Verify --plain-enums flag end-to-end Confirms the flag reaches generated output through the full command, mirroring the existing use-enums command-level test. --- .../Feature/Console/ModelTyperCommandTest.php | 11 ++++++ test/input/expectations/user-plain-enums.ts | 36 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 test/input/expectations/user-plain-enums.ts diff --git a/test/Tests/Feature/Console/ModelTyperCommandTest.php b/test/Tests/Feature/Console/ModelTyperCommandTest.php index 3441ddd..3cfb5fa 100644 --- a/test/Tests/Feature/Console/ModelTyperCommandTest.php +++ b/test/Tests/Feature/Console/ModelTyperCommandTest.php @@ -104,6 +104,17 @@ public function test_command_generates_use_enums_when_option_is_enabled() ])->expectsOutput($expected); } + public function test_command_generates_plain_enums_when_option_is_enabled() + { + $expected = $this->getExpectedContent('user-plain-enums.ts'); + + $this->artisan(ModelTyperCommand::class, [ + '--model' => User::class, + '--use-enums' => true, + '--plain-enums' => true, + ])->expectsOutput($expected); + } + public function test_command_generates_plurals_when_option_is_enabled() { $expected = $this->getExpectedContent('user-plurals.ts'); diff --git a/test/input/expectations/user-plain-enums.ts b/test/input/expectations/user-plain-enums.ts new file mode 100644 index 0000000..bc7a4e6 --- /dev/null +++ b/test/input/expectations/user-plain-enums.ts @@ -0,0 +1,36 @@ +export interface User { + // columns + id: number + name: string + email: string + email_verified_at: string | null + password?: string + remember_token?: string | null + created_at: string | null + updated_at: string | null + // mutators + role_traditional: string + role_new: string + role_enum: RolesEnum + role_enum_traditional: RolesEnum + score: number + score_nullable: number | null + role_or_string: Roles | stringEnum + // relations + notifications: DatabaseNotification[] + // counts + notifications_count: number + // exists + notifications_exists: boolean +} + +export enum Roles { + /** Can do anything */ + ADMIN = 'admin', + /** Standard readonly */ + USER = 'user', + /** Value that needs string escaping */ + USERCLASS = 'App\\Models\\User', +} + +export type RolesEnum = `${Roles}` From 59c65e7ca6636eeb7078a445d5e5afbb22aaeba6 Mon Sep 17 00:00:00 2001 From: Lorenzo Dal'Aqua Date: Fri, 31 Jul 2026 15:36:33 -0300 Subject: [PATCH 6/6] Document --plain-enums option Adds it to the Additional Options list and extends the existing const enum note to explain why it isn't readable at runtime and how --plain-enums opts out of that. --- readme.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/readme.md b/readme.md index 3340887..e58dee2 100644 --- a/readme.md +++ b/readme.md @@ -256,6 +256,7 @@ protected function firstName(): Attribute - --global : Generate typescript interfaces in a global namespace named models - --json : Output the result as json - --use-enums : Use typescript enums instead of object literals +- --plain-enums : Emit a plain enum instead of a const enum when --use-enums is set, so values are readable at runtime - --use-types : Use typescript types instead of interfaces - --plurals : Output model plurals - --no-relations : Do not include relations @@ -492,6 +493,10 @@ get detected and bring in your enum class with your comments: > ModelTyper uses Object Literals by default instead of TS > Enums [for opinionated reasons](https://maxheiber.medium.com/alternatives-to-typescript-enums-50e4c16600b1). But you can > use `--use-enums` option to use TS Enums instead of Object Literals. +> +> With `--use-enums`, ModelTyper emits `const enum` by default. Its members are inlined by the TypeScript compiler, so +> they aren't readable at runtime (e.g. `Object.values(...)` won't work). Add `--plain-enums` alongside `--use-enums` +> to emit a plain `enum` instead, which produces a real object you can read from at runtime. `app/Enums/UserRoleEnum.php`