A critical vulnerability was discovered in React Server Components (Next.js). Our systems remain protected but we advise to update packages to newest version. Learn More

Alex Boesen
Jul 7, 2020
  5063
(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
Building simple Opal tools for product search and content creation

Optimizely Opal tools make it easy for AI agents to call your APIs – in this post we’ll build a small ASP.NET host that exposes two of them: one fo...

Pär Wissmark | Dec 13, 2025 |

CMS Audiences - check all usage

Sometimes you want to check if an Audience from your CMS (former Visitor Group) has been used by which page(and which version of that page) Then yo...

Tuan Anh Hoang | Dec 12, 2025

Data Imports in Optimizely: Part 2 - Query data efficiently

One of the more time consuming parts of an import is looking up data to update. Naively, it is possible to use the PageCriteriaQueryService to quer...

Matt FitzGerald-Chamberlain | Dec 11, 2025 |

Beginner's Guide for Optimizely Backend Developers

Developing with Optimizely (formerly Episerver) requires more than just technical know‑how. It’s about respecting the editor’s perspective, ensurin...

MilosR | Dec 10, 2025