Type-safety across .Net and TypeScript – Constants

This is part 2 of the series about how we generate code from .Net for TypeScript:

  1. Why do we even bother?
  2. Generating TypeScript constants from .Net constants
  3. Finding the types used in communication between the .Net backend and the TypeScript client
  4. Generating TypeScript classes from .Net types
  5. Generating Angular Services from .Net WebApi Controllers
  6. TypeScript-friendly JSON serialization of F# types
  7. Testing JSON serialization and deserialization
  8. Putting all the parts together

We start with the simplest part, generating constants.

Generating TypeScript Constants from .Net Constants

We need to share constants defined in our .Net backend with our TypeScript client. We have constants that are strings, GUIDs, or wrappers around strings or GUIDs.

We do this with this code (explanation below):

module W2w.ConstantsWriter

open System
open System.Reflection
open System.IO

type ConstantInfo = { Name : string ; Value : string }

type ConstantClassInfo =
    {
        ClassName : string
        Constants : ConstantInfo []
    }

let (|String|StringWrapper|Guid|GuidWrapper|Unknown|) (value : obj) =
    match value with
    | null -> Unknown
    | :? string as s -> String s
    | :? Guid as g -> Guid g
    | _ ->
        let valueProperty = value.GetType().GetProperty("Value")

        match valueProperty with
        | null -> Unknown
        | _ ->
            match valueProperty.PropertyType with
            | x when x = typeof<Guid> ->
                let g : Guid = downcast (valueProperty.GetValue(value))
                GuidWrapper g
            | x when x = typeof<String> ->
                let s : string = downcast (valueProperty.GetValue(value))
                StringWrapper s
            | _ -> Unknown

let writeConstants (assemblies : Assembly []) (outputPath : string) =
    let types =
        assemblies
        |> Array.collect (fun a -> a.GetTypes())
        |> Array.sortBy (fun t -> t.Name)

    let staticClasses =
        types
        |> Array.filter (fun t -> t.IsClass && t.IsAbstract && t.IsSealed)

    let constantsPerClass =
        let getConstantsFromType (c : Type) =
            c.GetFields(BindingFlags.Public ||| BindingFlags.Static)
            |> Array.choose (fun field ->
                match field.GetValue(null) with
                | String s -> s |> Some
                | StringWrapper s -> s |> Some
                | Guid g -> g.ToString().ToUpper() |> Some
                | GuidWrapper g -> g.ToString().ToUpper() |> Some
                | Unknown -> None
                |> Option.map (fun v ->
                    {
                        ConstantInfo.Name = field.Name
                        Value = v
                    }))

        staticClasses
        |> Array.map (fun c ->
            {
                ConstantClassInfo.ClassName = c.Name
                Constants = getConstantsFromType c
            })


    let infos =
        constantsPerClass
        |> Array.filter (fun c -> c.Constants |> Array.isEmpty |> not)
        |> Array.map (fun c ->
            let constants =
                c.Constants
                |> Array.map (fun c ->
                    $"""    static {c.Name |> TypeWriter.toSmallCamelCase} = "{c.Value}" """)

            sprintf
                $"""
export class {c.ClassName}
{{
{constants |> String.concat Environment.NewLine}
}}""")

    let content =
        infos |> String.concat Environment.NewLine

    File.WriteAllText(outputPath, content, System.Text.UTF8Encoding())

F#

Line 7: ConstantInfo holds the name and the value of a constant.

Line 9: ConstantClassInfo holds the name of the class that contains constants and an array ConstantInfos.

Line 15: I use this active pattern to make finding constants a bit easier. Our constants come in different variants. They can be one of these:

  • A simple string constant: public static const MyConstant = "my constant value";
  • A simple GUID: public static readonly Guid MyConstant = Guid.Parse("...")
  • A wrapped string constant. We wrap identifiers in C# with classes or records so that we get better type safety. These wrappers always have a property with the name Value of the type string. This is a strict code convention we follow.
  • A wrapped GUID constant, analogue to the wrapped string constant.

The active pattern tries to match the specified value with one of the cases explained above.

At the moment, we only have to parse constants from C# code. When we add constants in F# code, I’ll extend the active pattern accordingly to find [<Literal<]s. Additionally, it doesn’t matter for us when we find some false positives and generate some “constants” that are not used by the client.

Line 35: The writeConstants function takes the assemblies it has to parse for constants and the path of the file to create. I’ll show how we get the assemblies in Part 6.

Line 36: We get all types from the specified assemblies and sort the types alphabetically. Sorting helps reduce noise when diffing in git and makes it easier to check the generated code when new constants are added.

Line 41: We get all the static classes. This is another convention that all constants that have to be shared have to be inside a static class.

Line 46: We get all public static fields and try to match them with one of the constant variants shown above.

Line 59: I’m not proud of using a null here, but because of the nesting, it is easier than using an Option. If you know a better solution, please let me know in the comments or on Twitter (@ursenzler).

Line 61: Filter out all fields that we didn’t recognise as a constant.

Line 63: we build the data that contains all classes with their contained constants.

Line 73: We filter out all classes that do not contain any constants.

Line 74-88: We write the TypeScript code for each class and its constants. We handle all constants as strings. That is good enough at the moment.

Line 90: Let’s write the file to disk.

The result looks like this (only a small fragment shown):

export class Activities
{
    static createdType = "cre" 
    static deletedType = "del" 
    static rejectedType = "rej" 
    static defaultValueSetType = "def" 
}

export class ColorSchemeConstants
{
    static defaultSchemeId = "1246972D-B526-4CF8-A5A3-FAA8F2470A78" 
}

In the next post, I’ll show you how we find the .Net types for which we need to generate their TypeScript equivalents.

This blog post is made possible with the support of Time Rocket, the product this blog post is about. Take a look (German only).

About the author

Urs Enzler

6 comments

By Urs Enzler

Recent Posts