Introduction
Some parts of my Knockout ViewModels are objects that have been returned from my WebApi controllers and transformed by The Knockout mapping plug-in. So how can I get TypeScript type checking at design time when these objects are dynamically created observables?
The mapping plug-in takes JSON data and dynamically converts it to nested collections of Knockout observables. It's not going to write any TypeScript definition files for me.
This combination of C#, TypeScript, Knockout, and the mapping plug-in creates a problem. I need a tool to read my server-side C# classes and create corresponding client-side TypeScript definitions. But as an extra complication, they have to be Knockout observables, not normal TypeScript types.
Background
I found that there are some tools that will read the metadata from a .NET assembly and generate corresponding TypeScript definitions. But none of them solved the extra complication of generating Knockout observables.
I put together a basic console app that uses reflection to read an assembly and generate the TypeScript definition file for the data transfer objects (DTOs) I am returning to my Knockout ViewModel.
Using the Code
First, I wrote two custom attributes and added them to my project:
public class TypeScriptKnockout
{
public class GenerateTypeScript : Attribute
{
public override string ToString()
{
return "TypeScriptKnockout.GenerateTypeScript";
}
}
public class NotObservable : Attribute
{
public override string ToString()
{
return "TypeScriptKnockout.NotObservable";
}
}
}
Then I use the first attribute, GenerateTypeScript
, on any classes I want to generate TypeScript for.
[TypeScriptKnockout.GenerateTypeScript]
public class MappingsViewModel : ISwisSecurityViewModel
{
public bool UpdateMode { get; set; }
public string Version { get; set; }
public string ServerName { get; set; }
public string Environment { get; set; }
public List<Profile> Profiles { get; set; }
public Profile SelectedProfile { get; set; }
public string ApplyToOtherEnvironment { get; set; }
[TypeScriptKnockout.NotObservable]
public GroupMappings GroupMappings { get; set; }
}
I use the second one, NotObservable
, to indicate any properties that I do not want to be observable. This will sometimes be necessary, since the Knockout mapping plug-in only makes the "leaf nodes" of any data structure observable (so any classes that contain more classes are marked as "not observable").
I build my project and then run the command line tool. I specify my assembly name, and the namespace I wish to search for classes.
TypeScriptKnockout Security.dll Security.Models.ViewModels
The tool reads the assembly, looks for classes and interfaces. It reads the properties and creates a TypeScript
property with a matching observable type:
switch (typeName)
{
case "Int32":
return notObservable ? "number" : "KnockoutObservable<number>";
case "String":
return notObservable ? "string" : "KnockoutObservable<string>";
case "DateTime":
return notObservable ? "Date" : "KnockoutObservable<Date>";
The generated types will be of KnockoutObservable<T>
, which is defined in the type definition file for Knockout.js, available from Definitely Typed.
The console app generates a file with all the interfaces for the namespace you specify:
class MappingsViewModel implements ISwisSecurityViewModel {
UpdateMode: KnockoutObservable<boolean>;
Version: KnockoutObservable<string>;
ServerName: KnockoutObservable<string>;
Environment: KnockoutObservable<string>;
Profiles: KnockoutObservableArray<Profile>;
SelectedProfile: KnockoutObservable<Profile>;
ApplyToOtherEnvironment: KnockoutObservable<string>;
GroupMappings: GroupMappings;
}
Now include the generated definition file and you will get type checking at design time, and intellisense. As you can see, Visual Studio knows SelectedProfile
is of type KnockoutObservable<Profile>
This console app method is pretty basic, and it would be nicer if it were called by the build script. But it works well enough.