The Practical Client
DataTyping in TypeScript
The TypeScript approach to data typing is different than what you're used to with server-side languages. This allows the language to integrate with other JavaScript libraries, but the results can surprise you.
Before I begin: With this column, I've started using Visual Studio Ultimate 2015 Preview for my Practical TypeScript case studies (primarily because I get better debugging support for TypeScript than I get in any other version of Visual Studio). That shouldn't make much difference to the code in this column, except that with Visual Studio 2015, I also pick up the latest version of TypeScript (version 1.3). That change might cause you to see some minor differences between your experience with TypeScript and what I report in this column.
With that out of the way …
Defining and Extending Classes
When working with TypeScript it's easy to forget that TypeScript classes aren't really like .NET classes. For example, here's a TypeScript class that includes a single numerical property called Id:
class CustomerBase
{
Id: number;
}
However, because I haven't initialized Id, it will be set to undefined (more on that later). As a decent human being, I should initialize the property, which I can do in one of two ways. I could just do it in the variable's declaration:
Id: number = -1;
However, for no particularly good reason that I can determine, I prefer to initialize variables in my class' constructor. So, the final version of my class looks like this:
class CustomerBase
{
constructor()
{
this.Id = -1;
}
Id: number;
}
I can create another class that (using TypeScript terminology) "extends" my CustomerBase class. This looks very much like the way I would create a derived class from a base class using inheritance in Visual Basic or C#. In my new TypeScript class, however, I must call the super method that triggers a call to the constructor in the class I'm extending. A class that extends CustomerBase and adds a new, initialized property called FullName would look like this:
class CustomerDto extends CustomerBase
{
constructor()
{
this.FullName = "";
super();
}
FullName: string;
}
The CustomerDto class will have two properties: the Id property it inherits from CustomerBase and the FullName property that it defines. The FullName property is being initialized in the constructor for CustomerDto while the Id property is initialized in the constructor for CustomerBase, triggered through the call to super.
Assigning Classes
Now I can define variables to point at objects created from those classes, instantiate the classes and set the class' properties:
var cBase: CustomerBase;
var cDto: CustomerDto;
cBase = new CustomerBase();
cBase.Id = 101;
cDto = new CustomerDto();
cDto.Id = 101;
cDto.FullName = "Peter Vogel"
This all looks very similar to what I might do in C# or Visual Basic. I might then try to assign one of my variables to the other. Depending on which variable is being assigned, I can write either of these two lines of code:
cDto = cbas;
cds = cDto;
However, only the second statement will compile. The first statement, which attempts to set the extended class variable to an instance of the original class generates a compile-time error: "Type 'CustomerBase' is not assignable to type 'CustomerDTO': Property 'FullName' is missing in type 'CustomerBase'" (the message will be different in earlier versions of TypeScript and Visual Studio, but will say the same thing).
An equivalent message in C# would be "Cannot implicitly convert…" The difference between the TypeScript "assignable" and the C# "convertible" says a great deal about TypeScript and the differences between the two languages.
If It's a Duck
TypeScript is a superset of JavaScript and JavaScript does not, of course, have the concept of data typing (let alone a mechanism for determining class compatibility). TypeScript has to figure out assignability on its own.
To determine if two classes are assignable, TypeScript uses what it calls "structural subtyping." With structural subtyping, TypeScript compares the properties and methods on the two objects. If the variable on the left-hand side of the equals sign has all the properties and the methods of the object on the right-hand side of the equals sign, then the two objects are assignable. This is a form of "duck typing" (from the old saying: "If it looks like a duck, walks like a duck and quacks like a duck, then it's a duck"). This is, of course, exactly what the message says: CustomerBase doesn't have the FullName property that CustomerDto does.
You get the same result in C# or Visual Basic, of course: You can't point a variable declared as a derived type at a base class; you can point a variable declared as a base type at a derived class.
With structural subtyping, it's perfectly OK for the object on the left-hand side to have more properties than the object on the right, it just can't have fewer properties. Obviously, any class that derives from another class will have all of the properties of the base interface, so my second line (assigning the base class to a variable type to the extended class) works just fine.
Structural subtyping lets TypeScript support extending classes defined in other libraries. If you extend some other class you can freely add properties and methods to your version of the class. When your class interacts with functions in the library from which the original class was drawn, TypeScript will consider your class as indistinguishable from the original class in the library: Thanks to the way extending works, your class will have all of the methods and properties of the original class, which is all structural subtyping requests.
"Fluid" DataTyping: Undefined
You can assign a CustomerBase class to a CustomerDto using a cast:
cDto = <CustomerDto> cBase;
Effectively, the cast gives TypeScript permission to ignore the "extra" properties (or methods) of the object on the right-hand side of the equals sign. Overriding the TypeScript judgement on assignability has some dangers, however: The FullName property on the cDto variable will now be undefined. When my CustomerBase class was instantiated, its constructor will have initialized the Id property. However, the assignment doesn't cause the CustomerDto constructor to run and the cast allows TypeScript to skip setting the FullName property.
What makes this result interesting is that, in TypeScript, undefined is a data type. Effectively, this means that while you can declare your variables as having a particular type, their type can be changed to undefined if the variable isn't set to a value. Therefore, to avoid problems, you should initialize your variables and check to see if a variable is undefined before using it. You can retrieve a variable's data type (as a string) using the typeof keyword, like this:
if ( typeof cDto.FullName === "undefined" )
{
...code for dealing with an undefined FullName property...
}
If you need to hold onto the type of a variable, you can hold the value in a string variable. This code will also work, for example:
var type: string;
type = typeof cDto.FullName;
...intervening code...
if ( type === "undefined" )
{
...code for dealing with an undefined FullName property...
}
While undefined is a data type (like string or number) it does have a significant limitation compared to other data types, like string or number: You can't declare variables using the undefined type. The following code isn't valid, for example:
var wrong: undefined;
There is, however, a literal that uses the undefined type: the literal called undefined. Because setting a variable to undefined changes the variable's data type, you can create variables typed as undefined by using the literal. Any of these examples results in undefinedVariable having its datatype set to undefined:
var undefinedVariable: string = undefined;
var undefinedVariable: number = undefined;
var undefinedVariable = undefined;
More usefully, the existence of the undefined literal allows you to check to see if a variable is undefined using this code (which I prefer to checking against a string because I can mistype the string "undefined"):
if ( typeof cDto.FullName === typeof undefined )
{
...code for dealing with an undefined FullName property...
}
Transferring Types
You can also use the typeof operator to set the data type of a variable. This can be useful when you call some function and aren't sure what type you're getting back (not unusual when calling a function from a library you haven't written). This code sets the data type of cName from the datatype of FullName:
var cName: typeof cDto.FullName;
If I try to use cName in my code with an inappropriate datatype, the compiler will give me data type errors based on the data type of FullName. Because cName is a string that means that I'll get a compile-time error for this code that attempts to store a number in cName:
cName = 2;
Those error messages, however, are based on what the compiler can determine at compile time. At run time, as you've seen, if you don't initialize FullName its datatype can be reset to the undefined type. As a result, you may get a different result when your code executes.
In the following code, for example, the compiler will be confident that both cNameFirst and cNameSecond are strings and issue error messages, appropriately:
var cNameFirst: typeof cDto.FullName;
cDto = <CustomerDto> cBase;
var cNameSecond: typeof cDto.FullName;
At run time, however, you'll get a different result. If you were to check the data type of cNameFirst at run time you would find that it's a string just as the compiler reported at compile time. But if you were to check the datatype of cNameSecond at run time, you'd find that its datatype is undefined because the intervening line has changed the type of FullName. As a result, you might be surprised by the way your code executes. As I said earlier, you may want to check that a variable is undefined before using it (think of it as the TypeScript equivalent of checking to see if a variable is set to nothing/Null).
TypeScript provides a flexible data typing system that supports integration with both JavaScript code and JavaScript libraries. However, as you've seen, data typing system is different from what you're used to in C# or Visual Basic. My best advice: Avoid letting your variables or properties become undefined (and check for undefined whenever it makes sense). You don't want to be surprised by your own code.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.