Targeting a different language

Generating TypeScript code

In this last part of the tutorial, you will learn how to generate the TypeScript equivalent of the C# code that we generated in the previous parts.

First, we have a new dependency to install, @yellicode/typescript.

npm install @yellicode/typescript --save-dev

We will leave our current code generation template as-is and create a new one for the TypeScript part. Create a new template file named typescript.template.ts and add the following entry to the codegenconfig.json file.

{
    "modelFile": "csharp-tutorial",
    "templateFile": "typescript.template.ts"
}

The template boilerplate

The boilerplate code for our template looks similar to the C# one. Copy the following contents into typescript.template.ts.

import { TextWriter } from '@yellicode/core';  
import { Generator } from '@yellicode/templating';  
import { TypeScriptWriter } from '@yellicode/typescript';
import { Model } from '@yellicode/elements';

Generator.generateFromModel({ outputFile: './output/typescript-model.ts' }, (output: TextWriter, model: Model) => {
    const ts = new TypeScriptWriter(output);
    // Generate enumerations
    model.getAllEnumerations().forEach((eachEnum) => {
        ts.writeEnumeration(eachEnum);
        ts.writeLine();
    });
    // Generate classes
    model.getAllClasses().forEach(cls => {
        ts.writeClassBlock(cls, () => {
            cls.ownedAttributes.forEach(att => {
                ts.writeProperty(att);
                ts.writeLine();
            });
        }, { export: true });
        ts.writeLine();
    });
}); 

If yellicode isn't still running, restart it using yellicode --watch and check if the output file is created. This output should already contain all your model types. However, there are two issues with it:

  1. Remember we applied the .NET profile in part III? You will notice that the generated output will contain at least one .NET specific type that the Typescript extension could not deal with (docs left out for brevity):
    export class Bug extends Issue {    
        public DiscoveredInversion!: string;
    
        public FixedInVersion!: Decimal; // Ouch! A Decimal in TypeScript?
    }
    
  2. The output contains classes that extend another class that is declared after it. When you would compile the generated ouutput, the TypeScript compiler will give a warning like "Class Issue' used before its declaration." because when, for example, Bug extends Issue, Issue must be declared before Bug.

Let's fix these one by one.

Fixing types

To fix our Decimal issue in TypeScript, let's first explain what's exactly going wrong here. By default, the TypeScript extension can only deal with types that it knows about. These are only the default UML primitives like boolean, integer and string. For other types, it does not know what TypeScript equivalent to write, so it just writes the type name as-is. And because we added a non-UML type from another extension - the .NET profile -, we need to tell the TypeScript extension what names to write for each .NET type that we depend on. This is where the TypeNameProvider interface comes to play.

The TypeScript extension contains its own implementation of the TypeNameProvider interface, the TypeScriptTypeNameProvider. We should extend this provider to support our .NET types. Create a new file named dotnet-typescript-type-name-provider.ts and add the following contents:

import { Type } from '@yellicode/elements';
import { TypeScriptTypeNameProvider } from '@yellicode/typescript';
import * as dotnet from '@yellicode/dotnet-profile';

/**
 * A custom TypeScriptTypeNameProvider that provides TypeScript type names for types in the 
 * Yellicode .NET profile.
 */
export class DotnetTypeScriptTypeNameProvider extends TypeScriptTypeNameProvider {
    protected /*override */ getTypeNameForType(type: Type | null, isDataType: boolean): string | null {
        if (dotnet.isDecimal(type)) {
            return 'number';
        }
        // Todo: deal with other NET or custom types here...

        // Always fallback to the default type name provided by the TypeScriptTypeNameProvider.
        return super.getTypeNameForType(type, isDataType);
    }
}

Then update the template typescript.template.ts to use our custom provider implementation:

  1. First, import the DotnetTypeScriptTypeNameProvider.
  2. import { DotnetTypeScriptTypeNameProvider } from './dotnet-typescript-type-name-provider';
  3. Then, replace the following line:
    const ts = new TypeScriptWriter(output);

    With these:

    const dotNetTsTypeNameProvider = new DotnetTypeScriptTypeNameProvider();
    const ts = new TypeScriptWriter(output, { typeNameProvider: dotNetTsTypeNameProvider });

Now regenerate your code and notice that our first problem is solved.

Fixing the order of classes

The second issue was that classes that extend another class that are declared after it. This would not be a problem with .NET code, but in TypeScript (Javascript), the order of declaration matters. The solution: find out dependencies between classes and make sure that they are written in the right order. You might be tempted to open up Yellicode Modeler and fix the order of your classes there, making the problem dissapear. But off course, this would cause a maintenance nightmare.

A better approach is to fix the order just before we generated the code, using a so-called model-transform. This sounds like a lot of work, but really just requires a few extra lines of code, because it's already in the box and is named the DependencySortTransform. Inside typescript.template.ts, update the import from @yellicode/elements as follows:

import { Model, DependencySortTransform } from '@yellicode/elements'; 

And above Generator.GenerateFromModel, add this line:

const transform = new DependencySortTransform();

Then update the Generator.GenerateFromModel as follows:

Generator.generateFromModel({ outputFile: './output/typescript-model.ts', modelTransform: transform}, (output: TextWriter, model: Model) => {

Now regenerate your code and notice the our first problem is solved as well! All your compiler errors are gone. This is how the full typescript template should look like right now:

import { TextWriter } from '@yellicode/core';  
import { Generator } from '@yellicode/templating';  
import { TypeScriptWriter } from '@yellicode/typescript';
import { Model, DependencySortTransform } from '@yellicode/elements';
import { DotnetTypeScriptTypeNameProvider } from './dotnet-typescript-type-name-provider';

const transform = new DependencySortTransform();

Generator.generateFromModel({ outputFile: './output/typescript-model.ts', modelTransform: transform}, (output: TextWriter, model: Model) => {
    const dotNetTsTypeNameProvider = new DotnetTypeScriptTypeNameProvider();
    const ts = new TypeScriptWriter(output, { typeNameProvider: dotNetTsTypeNameProvider });
    // Generate enumerations
    model.getAllEnumerations().forEach((eachEnum) => {
        ts.writeEnumeration(eachEnum);
        ts.writeLine();
    });
    // Generate classes
    model.getAllClasses().forEach(cls => {
        ts.writeClassBlock(cls, () => {
            cls.ownedAttributes.forEach(att => {
                ts.writeProperty(att);
                ts.writeLine();
            });
        }, { export: true });
        ts.writeLine();
    });
});  

Bonus: TypeScript interfaces instead of classes

There is actually another solution to the second problem. Because you might not really need to generate TypeScript classes at all! Instead - depending on how you are going to use them - it might be sufficient to generate TypeScript interfaces instead. With interfaces, the order of declaration does not matter (because interfaces are only used at design-time). And as a bonus, this solution also potentially results in a lot less Javascript when the TypeScript output is compiled.

Intead of using a DependencySortTransform, this solution uses a different transform named the ElementTypeTransform, which can make your classes impersonate interfaces for a moment. Import it from @yellicode/elements as follows:

import { Model, ElementTypeTransform, ElementType } from '@yellicode/elements';

And then create an instance:

const transform = new ElementTypeTransform(ElementType.class, ElementType.interface);

Generator.generateFromModel({ outputFile: './output/typescript-model.ts', modelTransform: transform }, (output: TextWriter, model: Model) => {

Now, make sure that your template supports generating interfaces as well:

// Generate interfaces
model.getAllInterfaces().forEach(iface => {
    ts.writeInterfaceBlock(iface, () => {
        iface.ownedAttributes.forEach(att => {
            ts.writeProperty(att);
            ts.writeLine();
        });
    }, { export: true });
    ts.writeLine();
});

What about my TypeScript naming conventions?

In TypeScript/Javascript, it is very common to use lowerCamelCase class- or property names. You can also changes the casing of your generated TypeScript using, you alreay guessed it, another transform. Update your template to import the UpperToLowerCamelCaseTransform and add a new import of the ModelTransformPipeline type. The latter one lets you chain multiple transforms.

import { Model, ElementTypeTransform, ElementType, UpperToLowerCamelCaseTransform, RenameTargets } from '@yellicode/elements';    
import { ModelTransformPipeline } from '@yellicode/core';

Then update the initialization of the transforms variable as follows and save the template:

const transform = new ModelTransformPipeline(
    new ElementTypeTransform(ElementType.class, ElementType.interface), // or DependencySortTransform
    new UpperToLowerCamelCaseTransform(RenameTargets.classes | RenameTargets.interfaces | RenameTargets.properties)
);    

What's next?

Thanks for taking the time to read through this tutorial. You now have the skills to start building your next model-driven application using Yellicode. We hope that it will be a great starting point for your next project, and that it will inspire the community to create and share new code generators for other technologies as well.

If you feel like doing more tutorials, you might also be interested in our full-stack application tutorial, in which you will learn how to use Yellicode to generate many parts of an Angular (2+) bookstore application with a .NET back-end.

Continue to the Bookstore Tutorial »