@@ -185,11 +185,13 @@ private function mapNativeCastToTypeScriptType(string $attribute): string {
185185 /**
186186 * Map a database column type to a TypeScript type.
187187 *
188- * @param \Illuminate\Support\Stringable $columnType
188+ * @param string $columnType
189189 *
190190 * @return string
191191 */
192- private function mapDatabaseTypeToTypeScriptType (Stringable $ columnType ): string {
192+ private function mapDatabaseTypeToTypeScriptType (string $ columnType ): string {
193+ $ columnType = Str::of ($ columnType );
194+
193195 return match (true ) {
194196 $ columnType ->startsWith ('bit ' ) => 'number ' ,
195197 $ columnType ->startsWith ('int ' ) => 'number ' ,
@@ -280,8 +282,6 @@ private function convertForeignKeyToFullyQualifiedModelName(string $attribute):
280282 * @return string
281283 */
282284 private function getTypeScriptType (stdClass $ columnSchema ): string {
283- $ columnType = Str::of ($ columnSchema ->Type );
284-
285285 if ($ this ->isAttributeRelation ($ columnSchema ->Field )) {
286286 $ fullyQualifiedRelatedModelName = $ this ->convertForeignKeyToFullyQualifiedModelName ($ columnSchema ->Field );
287287
@@ -294,6 +294,8 @@ private function getTypeScriptType(stdClass $columnSchema): string {
294294 // We generate new interfaces for any relational attributes.
295295 // That means we can recursively instantiate the current class to generate
296296 // as many interface definitions for relational attributes as we need.
297+ // We pass our existing convertedModelsMap instance here to prevent this class
298+ // mapping models we've already mapped in this current class instance.
297299 $ mappedType = (new self ($ fullyQualifiedRelatedModelName , $ this ->convertedModelsMap ))->generate ();
298300 }
299301 } else {
@@ -302,7 +304,7 @@ private function getTypeScriptType(stdClass $columnSchema): string {
302304 // simply map the database type to a TypeScript type.
303305 $ mappedType = $ this ->isAttributeNativelyCasted ($ columnSchema ->Field )
304306 ? $ this ->mapNativeCastToTypeScriptType ($ columnSchema ->Field )
305- : $ this ->mapDatabaseTypeToTypeScriptType ($ columnType );
307+ : $ this ->mapDatabaseTypeToTypeScriptType ($ columnSchema -> Type );
306308 }
307309
308310 // We can't do much with an unknown type.
@@ -347,7 +349,11 @@ public function includeHidden(bool $includeHidden): self {
347349 public function generate (): string {
348350 $ tableColumns = collect (DB ::select (DB ::raw ('SHOW COLUMNS FROM ' . $ this ->model ->getTable ())));
349351
350- $ interfaceName = Str::of ($ this ->fullyQualifiedModelName )->afterLast ('\\' )->toString ();
352+ $ interfaceName = Str::afterLast ($ this ->fullyQualifiedModelName , '\\' );
353+
354+ // At this point, we haven't technically generated the full TypeScript interface definition
355+ // for the target model. However, if the current model was to reference itself (which is valid),
356+ // without doing this here, it would cause an infinite loop.
351357 $ this ->convertedModelsMap [$ this ->fullyQualifiedModelName ] = $ interfaceName ;
352358
353359 // The output buffer always needs to start with the first `interface X {` line.
@@ -361,24 +367,38 @@ public function generate(): string {
361367 $ relationName = $ this ->convertForeignKeyToPredictedRelationName ($ columnSchema ->Field );
362368 $ generatedTypeScriptType = Str::of ($ this ->getTypeScriptType ($ columnSchema ));
363369
364- $ isRelationNewInterfaceDefinition = $ generatedTypeScriptType ->startsWith ('interface ' );
370+ // We know we've just generated a new interface if the generated TypeScript type
371+ // starts with 'interface '. If it doesn't, we'll be referring to a TypeScript type
372+ // that's been previously generated.
373+ $ isRelationInterfaceDefinition = $ generatedTypeScriptType ->startsWith ('interface ' );
365374
366- if ($ isRelationNewInterfaceDefinition ) {
375+ if ($ isRelationInterfaceDefinition ) {
376+ // interface User { => User
367377 $ generatedInterfaceName = $ generatedTypeScriptType
368378 ->after ('interface ' )
369379 ->before (' { ' );
370380
371381 $ fullyQualifiedRelatedModelName = $ this ->convertForeignKeyToFullyQualifiedModelName ($ columnSchema ->Field );
382+ // We've just generated a new interface, we'll want to make sure our class doesn't attempt to
383+ // generate it again by adding it to our convertedModelsMap.
372384 $ this ->convertedModelsMap [$ fullyQualifiedRelatedModelName ] = $ generatedInterfaceName ->toString ();
373385
386+ // Columns aren't always required. To make sure we're not losing other type metadata,
387+ // we'll append everything after the end of the interface definition onto the new
388+ // interface name we've generated.
374389 $ generatedType = $ generatedInterfaceName ->append ($ generatedTypeScriptType ->afterLast ('} ' ));
375390 } else {
391+ // If we don't have a new relation interface definition, we'll have the interface name definition,
392+ // retrieved from the convertedModelsMap (as well as any type metadata). We can use that value directly.
376393 $ generatedType = $ generatedTypeScriptType ;
377394 }
378395
396+ // Append the relation to the interface we're generating.
379397 $ outputBuffer ->push (sprintf (' %s: %s; ' , $ relationName , $ generatedType ));
380398
381- if ($ isRelationNewInterfaceDefinition ) {
399+ // If we've generated a new interface, we'll want to append it above the current
400+ // interface we're in the process of generating.
401+ if ($ isRelationInterfaceDefinition ) {
382402 // Add an empty line so related interfaces aren't directly after each other.
383403 $ outputBuffer ->prepend ('' );
384404
@@ -395,6 +415,7 @@ public function generate(): string {
395415 ->each (fn ($ str ) => $ outputBuffer ->prepend ($ str ));
396416 }
397417 } else {
418+ // Append the column name, and the TypeScript type to the interface we're generating.
398419 $ outputBuffer ->push (sprintf (' %s: %s; ' , $ columnSchema ->Field , $ this ->getTypeScriptType ($ columnSchema )));
399420 }
400421 });
0 commit comments