November Happy Hour will be moved to Thursday December 5th.

Alex Boesen
Jul 7, 2020
  4254
(3 votes)

Generate Typescript interfaces for pages and blocks

At Alm Brand we are using Vue.js together with the content delivery api for our Single Page Applications(SPA) that powers parts of our website and App, when we starting doing this we took a lot of inspiration from episerver musicfestival vue template and we ended up with what we call the "Vue-Epi Framework" (yea the name needs work).

One of our key differences with the template, and the focus for this blog post, is we are using TypeScript which leads to the question:
How do we get that sweet typing for our episerver models when using the content delivery api? read on and find out!

Typescript all the things

The first thing to do is find or create the folder where your typescript definitions lives and create a file named content.d.ts that maps all the base/common types/properties like the short sample below

/** interface for ContentLanguage coming from episerver content delivery api */
export interface ContentLanguage {
    /** the link to the this langauge version of the current content */
    link: string;
    /** the localized displayName for the language */
    displayName: string;
    /** the ISO name for the language */
    name: string;
}

/** interface for ContentReference coming from episerver content delivery api */
export interface ContentReference {
    /** the episerver content id */
    id: number;
    /** the episerver content work id */
    workId: number;
    /** the guid id of the content */
    guidValue: string;
    /** the content providerName */
    providerName?: string;
    /** url to the content (block points to the frontpage) */
    url: string;
}

(full file here

That is used by the ContentTypeCodeGenerator, a simple mapper that scans our assemblies for content, enums and "other things" to turn them into typescript interfaces and saving those to a file.

private void GenerateTypescriptInterfaces()
{
    IEnumerable<Assembly> assemblies = GetAssemblies();
    IEnumerable<Type> types = assemblies.SelectMany(a => GetTypesFromAssembly(a)).ToList();
    var contentTypes = types.Where(t => t.GetCustomAttribute<ContentTypeAttribute>() != null && !typeof(IContentMedia).IsAssignableFrom(t))
    StringBuilder builder = new StringBuilder();
    builder.AppendLine("import { IContent, ContentLanguage, ContentReference } from './content'");
    GenerateTypescriptEnums(types, builder)
    foreach (var contentType in contentTypes)
    {
        Logger.Information("Adding {ContentType} as typescript interface", contentType.Name);
        builder.AppendLine($"export interface {contentType.Name} extends IContent {{");
        AddProperties(contentType);
        builder.AppendLine("}")
    
    var fileText = builder.ToString();
    if (HasFileContentChanged(fileText))
    {
        File.WriteAllText(FilePath, fileText);
    }
}

(full class here)

most of the secret sauce is in the GetDataType method that maps an property to an typescript type

string GetDataType(Type contentType, PropertyInfo property)
{
    if (TypeMappings.TryGetValue(property.PropertyType, out var func))
    {
        return func(contentType, property);
    }
    else
    {
        if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType))
        {
            return FindListType(contentType, property);
        }
        return property.PropertyType.Name;
    }
}

As you can see we either lookup the type in TypeMappings or return the name of the property type(mostly used with Enum and Block properties on models)
the TypeMappings is a simple Dictionary with mappings as can seen below 

private static IDictionary<Type, Func<Type, PropertyInfo, string>> CreateTypeMapping()
{
    var mappingDictionary = new Dictionary<Type, Func<Type, PropertyInfo, string>>();
    mappingDictionary.Add(typeof(string), (t, p) => "string");
    mappingDictionary.Add(typeof(int), (t, p) => "number");
    mappingDictionary.Add(typeof(int?), (t, p) => "number");
    mappingDictionary.Add(typeof(decimal), (t, p) => "number");
    mappingDictionary.Add(typeof(decimal?), (t, p) => "number");
    mappingDictionary.Add(typeof(float), (t, p) => "number");
    mappingDictionary.Add(typeof(float?), (t, p) => "number");
    mappingDictionary.Add(typeof(double), (t, p) => "number");
    mappingDictionary.Add(typeof(double?), (t, p) => "number");
    mappingDictionary.Add(typeof(bool), (t, p) => "boolean");
    mappingDictionary.Add(typeof(bool?), (t, p) => "boolean");
    mappingDictionary.Add(typeof(DateTime), (t, p) => "string");
    mappingDictionary.Add(typeof(DateTime?), (t, p) => "string");
    mappingDictionary.Add(typeof(ContentReference), (t, p) => GetContentReferenceType(t, p));
    mappingDictionary.Add(typeof(PageReference), (t, p) => "ContentReference");
    mappingDictionary.Add(typeof(ContentArea), (t, p) => "Array<IContent>");
    mappingDictionary.Add(typeof(LinkItemCollection), (t, p) => "Array<string>");
    mappingDictionary.Add(typeof(PropertyContentReferenceList), (t, p) => "Array<IContent>");
    mappingDictionary.Add(typeof(Url), (t, p) => "string");
    mappingDictionary.Add(typeof(XhtmlString), (t, p) => "string");
    return mappingDictionary;
    string GetContentReferenceType(Type contentType, PropertyInfo property)
    {
        //we convert ContentReferences that point to Images to an url in content delivery api
        var uiHint = property.GetCustomAttribute<UIHintAttribute>();
        if (uiHint?.UIHint == UIHint.Image)
        {
            return "string";
        }
        return "ContentReference";
    }
}

the resulting file is like this (only with much more data of course)

export enum TileVariantEnum {
  Navigation=1,
  Call=2,
}

export interface TileBlock extends IContent {
  tileVariant: TileVariantEnum;
  title: string;
  icon: string;
  link: string;
  phoneNumber: string;
  renderHtml: boolean;
  hide: boolean;
}

You can now import your models in your typescript code and get that sweet, sweet typing experience.

 

import { Vue, Component } from "vue-property-decorator";
import { PropType } from "vue";
import { mapState } from "vuex";
import template from './TileBlock.vue';
import { TileBlock, TileVariantEnum } from "@scripts/definitions/episerver/content-types";
import { AbLink } from "@scripts/app/components/sharedcomponents/baseComponents/components";

@Component({
    name: 'TileBlockComponent',
    mixins:[template],
    components: {
        AbLink
    },
    computed: mapState<any>({
        isEditable: state => state.epiContext.isEditable,
        parentModel: state => state.epiContent.model
    }),
    props: {
        model: Object as PropType<TileBlock>
    },
})

export default class TileBlockComponent extends Vue {
    model: TileBlock;
    path: string = "";

    mounted() {
        this.path = this.model.link;
        if (this.model.tileVariant === TileVariantEnum.Call) {
            this.path = this.model.phoneNumber;
        } 
    }
}

There is some small cavats with this, it assumes you are using SetFlattenPropertyModel(true) and SetExtendedContentTypeModel(true) with content delivery api and some of the mappings like ContentReference to string when it have the UIHint.Image attribute also requires you to expand the content delivery api with an custom IPropertyModelConverter to actually make the conversion but that another blog post ;) 

we call this code from both an InitializableModule when on localhost for devs to always have an up to date version of the file and for our deployments we have an CI/CD pipeline that does the same thing to make sure the frontend can compile with the code that are currently building.

Play around with ContentTypeCodeGenerator and content.d.ts and see if it is something you can use :) 

Jul 07, 2020

Comments

Michael Clausing
Michael Clausing Jul 7, 2020 08:41 PM

We are working on an addon for content management using react, and have had some success using Reinforced.Typings to generate Typescript interfaces/enums. It took a little trial an error to find the correct settings, but I think it works pretty well now.

Please login to comment.
Latest blogs
Optimizely SaaS CMS + Coveo Search Page

Short on time but need a listing feature with filters, pagination, and sorting? Create a fully functional Coveo-powered search page driven by data...

Damian Smutek | Nov 21, 2024 | Syndicated blog

Optimizely SaaS CMS DAM Picker (Interim)

Simplify your Optimizely SaaS CMS workflow with the Interim DAM Picker Chrome extension. Seamlessly integrate your DAM system, streamlining asset...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Optimizely CMS Roadmap

Explore Optimizely CMS's latest roadmap, packed with developer-focused updates. From SaaS speed to Visual Builder enhancements, developer tooling...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Set Default Culture in Optimizely CMS 12

Take control over culture-specific operations like date and time formatting.

Tomas Hensrud Gulla | Nov 15, 2024 | Syndicated blog

I'm running Optimizely CMS on .NET 9!

It works 🎉

Tomas Hensrud Gulla | Nov 12, 2024 | Syndicated blog

Recraft's image generation with AI-Assistant for Optimizely

Recraft V3 model is outperforming all other models in the image generation space and we are happy to share: Recraft's new model is now available fo...

Luc Gosso (MVP) | Nov 8, 2024 | Syndicated blog