Alex Boesen
Jul 7, 2020
  5322
(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
Announcing new library: SettingsManager

When you run .net app, there have been a few ways to store settings. Those can be set via appSettings.json, or via Azure Portal AppService...

Quan Mai | Apr 30, 2026

From Prompting to Production: Optimizely Opal University Cohort and the Future of Agentic MarTech

Most organizations today are still playing with AI. They experiment with prompts, test ideas in isolated chats, and occasionally automate a task or...

Augusto Davalos | Apr 28, 2026

Six Compelling Reasons for Upgrading to CMS 13

Most software updates ask you to keep up. Optimizely CMS 13 asks something different — it asks whether your digital strategy is built for a world...

Muhammad Talha | Apr 28, 2026

Optimizely CMS 13 breaking changes: GetContentTypePropertyDisplayName

When upgrading from CMS 12 to 13, resolving property display names may not work as before. Here’s what changed.

Tomas Hensrud Gulla | Apr 27, 2026 |