The Practical Client
TypeScript 2.0: Even Better Data Typing and Class Discriminants
If you care about data typing and want to avoid null/undefined errors, there's a lot you're going to like in TypeScript 2.0. And, if you like creating general purpose functions that can work with a variety of classes, there's something here for you, too.
You can download TypeScript 2.0 or get the TypeScript 2.0 compiler through NuGet -- provided, of course, you're working in Visual Studio 2015 with Update 3 or Visual Studio Code. Even if that doesn't describe you, if you're a server-side/datatyping programmer thinking about moving into client-side coding, TypeScript might well be in your future (the TypeScript team claimed 2 million downloads in August of 2016, just through npm).
Here's why this release will matter to TypeScript programmers who value type checking and want to avoid errors related to variables set to null or undefined. TypeScript 2.0 also provides a new way to distinguish between related classes.
Better Data Typing
The most common error with JavaScript code (other than mistaking what the this keyword refers to) is probably errors with variables that are currently set to the null or undefined types. In previous versions of TypeScript a variable declared as some type could always be used with a variable set to null or undefined allowing null/undefined values should be to propagate through your code.
In TypeScript 2.0, that's no longer the case. If you want a variable to accept null or undefined then you have to declare that by using a union type.
In the following code, the compiler will not allow the variable strictSample to be set to anything but Boolean value (no null or undefined), will allow the variable flexibleSample to be set to a Boolean value or undefined (no null) and will allow veryFlexibleSample should be to be set to a Boolean value or either undefined or null:
var strictSample: boolean;
var flexibleSample: boolean | undefined;
var veryFlexibleSample: boolean | null | undefined;
One wrinkle (and then one caveat): Because this is potentially a breaking change in TypeScript (it redefines all previous declarations), you have to turn this option on. You can do that by adding --strictNullChecks to the options in your compiler's command line but it's probably easier just to set the option in your tsconfig.json file like this:
{
"compilerOptions": {
"strictNullChecks": true,
Which brings me to the caveat: This is a compiler-time check, not a run-time check. It's probably still possible to insert null and undefined values into these variables at run-time if you're tricky enough. This option does not add a bunch of tests for undefined or null to your generated code.
Collateral Enhancements
To make these restrictions work at compile time, the compiler relies on enhanced flow analysis capabilities. The 2.0 version of the TypeScript compiler can do a much better job of figuring out what your code will do at runtime. This means that, over and above stricter type checking, you can expect better compile-time reports on errors in your code (the compiler will do a better job on reporting how use you use uninitialized variables, for example).
This change also alters how your classes use optional parameters and provide support for optional properties and methods in your classes. With parameters, regardless of how you declare an optional parameter for a method, the undefined type is automatically added to the parameter's declaration. By the time that the compiler is finished with them, these two declarations are identical, for example:
function UpdateCustomer(cust?: Customer): boolean {...}
function UpdateCustomer(cust?: Customer | undefined): boolean {...}
But you can now declare optional properties and methods in a class. You indicate that by adding a question mark to the end of a property or method's name. This class declares both the age property and the calculateAge method as optional, for example:
public class Customer {
id: string;
name: string;
age?: number;
calculateAge?(): number
With this declaration, a valid Customer object can omit the age property and skip providing an implementation for the calculateAge method. When a class does omit those members, clients that access that property or method will find them set to undefined.
To ensure that the property or method is present, you can use a type guard (a test for data type or presence) around code that uses optional properties my methods. Using my Customer object, I could write code like this:
function UpdateCustomer(cust: Customer)
{
if (cust.age && cust.calculateAge)
{
//...use age and calculateAge
}
The if test on age and calculateAge will only pass if both are not set to undefined and, as a result, the code inside the code block is guaranteed to find a value in age and an implementation in calculateAge.
If you think the compiler can't determine your intentions regarding null and undecided from your code, you can signal that your variable won't accept null or undefined by adding an exclamation mark (!) to the end of your variable name. This code asserts that the variable cust may not be null or undefined when accessing the isValid property:
if (cust!.isValid) {...}
This change also impacts both expressions (a statement that can be evaluated down to a single value -- x + y is an expression, for example) and inferred data types. In TypeScript, the output of an expression is always assigned a data type. With strictNullChecks turned on, that data type does not include null or undefined -- it will always be some strict data type even if values in the expression accept null or undefined...with one exception.
That exception is around expressions that include logical ANDs (&&) or logical ORs (||). Logical ANDs will automatically propagate undefined or null types from the right-hand side of an equals sign to the left-hand side. So if one of the inputs of a logical AND includes null or undefined in its datatype then the result of the logical AND will also include null or undefined. Logical OR (||), on the other hand, strips undefined and null out of the type of the result even if the inputs included it.
Finally, if you let the compiler infer your data type by simply assigning a value to a newly declared variable, the compiler will not widen the type to include null or undefined. This means that the variable in the following code is especially useless because it can only be assigned a single value: null. Without strictNullChecks, the variable's data type would have been expanded to the any type and the variable could be assigned any value at all:
var limited = null;
Determining Types by Property Values
You can now declare discriminant properties (properties whose value identifies a class) when declaring classes and then use those values in type guards to ensure that your code is only dealing with a specific class. This is equivalent to having a table in a database that holds several different kinds of data and has a column that distinguishes between those types (e.g. a PayType column that distinguished between salaried and hourly employees).
For example, assume that you want to write a function that will handle two kinds of objects: Parts and Services. You define your Part class like this:
class Part {
ProductType: "Part";
Price: number;
}
And then add a Service class that looks like this:
class Service {
ProductType: "Service";
HourlyCharge: number;
CalloutRate: number;
}
The ProductType property in both classes is a string property that can only be set to one value ("Part" in the Part class, "Service" in the Service class). That ProductType property acts as a discriminant property for the two classes and doesn't need to be set when instantiating a class. This code, for example, creates a Service class and just sets the two properties on the class (the ProductType property is set automatically):
var serv: Service;
serv = new Service();
serv.CalloutRate = 50;
serv.HourlyCharge = 5;
You can now declare a parameter to a function as a union of Part and Service, like this:
function CalculatePrice (prod: Part | Service, quantity: number) {
var cost: number;
Within this class, you can test to see which kind of object you've been passed by looking at the ProductType property and do the right thing with the object. That test acts as a type guard to ensure that, within the test's code block, you have the right object. In addition, with TypeScript 2.0, switch statements can act as type guards.
As an example, in this code, my switch statement checks to see whether I have a Part or a Service. Within the code block for "Part," IntelliSense will show me only the properties for Part objects:
switch (prod.ProductType) {
case "Part":
cost = prod.Price * quantity;
break;
case "Service":
cost = prod.HourlyCharge * quantity + prod.CalloutRate;
break;
}
There's more, of course. For example, TypeScript 2.0 now supports the readonly keyword which can be used with fields (variables declared outside of any method or property) to specify a variable that can only have its value set in a class' constructor. Any property declared with a getter and without a setter is assumed to be a readonly property whose value can only be set in the class' constructor.
If you like data typing and were thinking of moving to client-side programming, TypeScript has always been an attractive option. TypeScript 2.0 just makes it more attractive.
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/.