TypeScript 2.8's conditional types can be used to create compile-time inference assertions, which can be used to writetests that verify the behavior of TypeScript's inference on your API.
Correct me if I'm wrong but it looks like @logProperty decorator defines property on prototype making things a bit wrong: as I can see running code from typescript playground this is a window object and then once you make several objects of type Person, changing the name for one of them will resolve in changing the name for all the others (since this property lives in prototype)? Property Description; value: any: Read-Only. The current value of the control. For a FormControl, the current value.; For an enabled FormGroup, the values of enabled controls as an object with a key-value pair for each member of the group.
This is a very powerful tool for improving the usability of your API. To demonstrate, let's imagine that we are buildinga 'pluck' function:
While this may look like a perfectly good type signature and implementation, when we consider the usability of thereturned value's type, there are going to be some surprises—especially in TypeScript's --strict
mode.
For this example, let's assume we have the following interface:
If we use this naive version of pluck
, we'll see that there are some unexpected consequences of type inference.
Even though the intent of the API is to return a structure that's a subset of the plucked object, it has two unintendedusability consequences with TypeScript's inference behavior:
- The returned object has members are all of the type
T | undefined
. This will cause frustrations when using thispluck
function in--strict
mode. - Keys that are not specified are optionally present in the returned object's type. We should be able to know thatthe
bool
key will never be present in the return type.
How can we verify compile-time inference behavior?
If we wanted API usability/behavior to act a certain way at runtime, we could write a few tests which assert thatbehavior and then modify our implementation of pluck
so that our desired behavior is verified. However, since thebehavior we want is something that is determined at compile-time, we need to resort to telling the compiler toperform these assertions for us at compile-time.
Using TypeScript 2.8's conditional types, we can define the shape and inference behavior of the API we want to buildprior to actually implementing it. Think of this as a sort of TDD for your types.
We can do this by (1) asserting the inferred value is assignable to the types that we want (conditional types comein handy here), and (2) cause the compiler to reject code at compile time when these assertions are not true.
As a tiny example, if we want to write a compile-time test that asserts 'this value should really be inferred as anumber,' we can do the following:
Using these assertions to make a better pluck
Applying this technique to our API, we can describe the behavior we want for our case #1 (members having an unwanted | undefined
):
Excellent, now that we have a compile-time error that asserts our behavior, we can redefine pluck
's type signature to bemore accurate.
This compiles, which means our problem #1 is solved! Unfortunately, this signature is a lie. While we 'fixed' #1, westill need to deal with our case #2, where missing members are still present in the returned type.
To check for this, we need a few type devices to fail compile if a key is present in a type:
Asserting the absence of a key
There are a few type operations that we need to know in order to check if an object does not have a key.
First off, here's a brief refresher on the building blocks we'll use:
So let's build a type device that evaluates to true
when an object T
does not have a key K
:
Putting it all together
Now with this TrueIfMissing
type device, we can assert that we do not want to have certain keys present in thereturned object from our pluck
:
Finally we can create a version of pluck that satisfies all of our usability concerns:
Why go through all this work?
When we have automated tests which assert the behavior of our code, we gain confidence that changes to our software willnot introduce regressions. However, when designing an API which is meant to leverage type inference to gain usability,there hasn't really been an obvious way of doing this.
This technique allows us to effectively test how TypeScript performs its inference for users of our API. We canbuild a test module which makes assertions about our desired type inference, and if the test file compiles successfully,our assertions are correct! That way, if our API subtly changes in a way that makes return values or callback parametersharder to infer, we can be alerted to this by a failure to compile.
If you happen to know of other techniques that can be used to accomplish this sort of compile-time assertion, I'd loveto hear them! Please reach out and let me know!
-->January 2015
Volume 30 Number 1
By Peter Vogel | January 2015
In many ways, it’s useful to think of TypeScript on its own merits. The TypeScript language specification refers to TypeScipt as “a syntactic sugar for JavaScript.” That’s true and probably an essential step in reaching to the language’s target audience—client-side developers currently using JavaScript.
And you do need to understand JavaScript before you can understand TypeScript. In fact, the language specification (you can read it at bit.ly/1xH1m5B) often describes TypeScript constructs in terms of the resulting JavaScript code. But it’s equally useful to think of TypeScript as a language on its own that shares features with JavaScript.
For example, like C#, TypeScript is a data-typed language, which gives you IntelliSense support and compile-time checking, among other features. Like C#, TypeScript includes generic and lambda expressions (or their equivalent).
But TypeScript, of course, is not C#. Understanding what’s unique about TypeScript is as important as understanding what TypeScript shares with the server-side language you’re currently using. The TypeScript type system is different (and simpler) than C#. TypeScript leverages its understanding of other object models in a unique way and executes inheritance differently than C#. And because TypeScript compiles to JavaScript, TypeScript shares many of its fundamentals with JavaScript, unlike C#.
The question then remains, “Would you rather write your client-side code in this language or in JavaScript?”
TypeScript Is Data-Typed
TypeScript doesn’t have many built-in data types you can use to declare variables—just string, number and Boolean. Those three types are a subtype of the any type (which you can also use when declaring variables). You can set or test variables declared with those four types against the types null or undefined. You can also declare methods as void, indicating they don’t return a value.
This example declares a variable as string:
You can extend this simple type system with enumerated values and four kinds of object types: interfaces, classes, arrays and functions. For example, the following code defines an interface (one kind of object type) with the name ICustomerShort. The interface includes two members: a property called Id and a method called CalculateDiscount:
As in C#, you can use interfaces when declaring variables and return types. This example declares the variable cs as type ICustomerShort:
You can also define object types as classes, which, unlike interfaces, can contain executable code. This example defines a class called CustomerShort with one property and one method:
Like more recent versions of C#, it’s not necessary to provide implementation code when defining a property. The simple declaration of the name and type is sufficient. Classes can implement one or more interfaces, as shown in Figure 1, which adds my ICustomerShort interface, with its property, to my CustomerShort class.
Figure 1 Add an Interface to a Class
As Figure 1 shows, the syntax for implementing an interface is as simple in TypeScript as in C#. To implement the interface’s members you simply add members with the same name instead of tying the interface name to the relevant class’ members. In this example, I simply added Id and CalculateDiscount to the class to implement ICustomerShort. TypeScript also lets you use object type literals. This code sets the variable cst to an object literal containing one property and one method:
This example uses an object type to specify the return value of the UpdateStatus method:
Besides object types (class, interface, literal and array), you can also define function types that describe a function’s signature. The following code rewrites CalculateDiscount from my CustomerShort class to accept a single parameter called discountAmount:
That parameter is defined using a function type that accepts two parameters (one of string, one of boolean) and returns a number. If you’re a C# developer, you might find that the syntax looks much like a lambda expression.
A class that implements this interface would look something like Figure 2.
Figure 2 This Class Implements the Proper Interface
Like the recent versions of C#, TypeScript also infers the datatype of a variable from the value to which the variable is initialized. In this example, TypeScript will assume the variable myCust is of CustomerShort:
Like C#, you can declare variables using an interface and then set the variable to an object that implements that interface:
Finally, you can also use type parameters (which look suspiciously like generics in C#) to let the invoking code specify the data type to be used. This example lets the code that creates the class set the datatype of the Id property:
This code sets the datatype of the Id property to a string before using it:
To isolate classes, interfaces and other public members and avoid name collisions, you can declare these constructs inside modules much like C# namespaces. You’ll have to flag those items you want to make available to other modules with the export keyword. The module in Figure 3 exports two interfaces and a class.
Figure 3 Export Two Interfaces and One Class
To use the exported components, you can prefix the component name with the module name as in this example:
Or you can use the TypeScript import keyword to establish a shortcut to the module:
TypeScript Is Flexible About Data Typing
All this should look familiar if you’re a C# programmer, except perhaps the reversal of variable declarations (variable name first, data type second) and object literals. However, virtually all data typing in TypeScript is optional. The specification describes the data types as “annotations.” If you omit data types (and TypeScript doesn’t infer the data type), data types default to the any type.
TypeScript doesn’t require strict datatype matching, either. TypeScript uses what the specification calls “structural subtyping” to determine compatibility. This is similar to what’s often called “duck typing.” In TypeScript, two classes are considered identical if they have members with the same types. For example, here’s a CustomerShort class that implements an interface called ICustomerShort:
Here’s a class called CustomerDeviant that looks similar to my CustomerShort class:
Thanks to structural subtyping, I can use CustomerDevient with variables defined with my CustomerShort class or ICustomerShort interface. These examples use CustomerDeviant interchangeably with variables declared as CustomerShort or ICustomerShort:
This flexibility lets you assign TypeScript object literals to variables declared as classes or interfaces, provided they’re structurally compatible, as they are here:
This leads into TypeScript-specific features around apparent types, supertypes and subtypes leading to the general issue of assignability, which I’ll skip here. Those features would allow CustomerDeviant, for example, to have members that aren’t present in CustomerShort without causing my sample code to fail.
TypeScript Has Class
The TypeScript specification refers to the language as implementing “the class pattern [using] prototype chains to implement many variations on object-oriented inheritance mechanisms.” In practice, it means TypeScript isn’t only data-typed, but effectively object-oriented.
In the same way that a C# interface can inherit from a base interface, a TypeScript interface can extend another interface—even if that other interface is defined in a different module. This example extends the ICustomerShort interface to create a new interface called ICustomerLong:
The ICustomerLong interface will have two members: FullName and Id. In the merged interface, the members from the interface appear first. Therefore, my ICustomerLong interface is equivalent to this interface:
A class that implements ICustomerLong would need both properties:
Classes can extend other classes in the same way one interface can extend another. The class in Figure 4 extends CustomerShort and adds a new property to the definition. It uses explicit getters and setters to define the properties (although not in a particularly useful way).
Figure 4 Properties Defined with Getters and Setters
TypeScript enforces the best practice of accessing internal fields (like id and fullName) through a reference to the class (this). Classes can also have constructor functions that include a feature C# has just adopted: automatic definition of fields. The constructor function in a TypeScript class must be named constructor and its public parameters are automatically defined as properties and initialized from the values passed to them. In this example, the constructor accepts a single parameter called Company of type string:
Because the Company parameter is defined as public, the class also gets a public property called Company initialized from the value passed to the constructor. Thanks to that feature, the variable comp will be set to “PH&VIS,” as in this example:
Declaring a constructor’s parameter as private creates an internal property it can only be accessed from code inside members of the class through the keyword this. If the parameter isn’t declared as public or private, no property is generated.
Your class must have a constructor. As in C#, if you don’t provide one, one will be provided for you. If your class extends another class, any constructor you create must include a call to super. This calls the constructor on the class it’s extending. This example includes a constructor with a super call that provides parameters to the base class’ constructor:
TypeScript Inherits Differently
Again, this will all look familiar to you if you’re a C# programmer, except for some funny keywords (extends). But, again, extending a class or an interface isn’t quite the same thing as the inheritance mechanisms in C#. The TypeScript specification uses the usual terms for the class being extended (“base class”) and the class that extends it (“derived class”). However, the specification refers to a class’ “heritage specification,” for example, instead of using the word “inheritance.”
To begin with, TypeScript has fewer options than C# when it comes to defining base classes. You can’t declare the class or members as non-overrideable, abstract or virtual (though interfaces provide much of the functionality that a virtual base class provides).
There’s no way to prevent some members from not being inherited. A derived class inherits all members of the base class, including public and private members (all public members of the base class are overrideable while private members are not). To override a public member, simply define a member in the derived class with the same signature. While you can use the super keyword to access a public method from a derived class, you can’t access a property in the base class using super (though you can override the property).
Typescript Private Abstract Property
TypeScript lets you augment an interface by simply declaring an interface with an identical name and new members. This lets you extend existing JavaScript code without creating a new named type. The example in Figure 5 defines the ICustomerMerge interface through two separate interface definitions and then implements the interface in a class.
Figure 5 The ICustomerMerge Interface Defined Through Two Interface Definitions
Classes can also extend other classes, but not interfaces. In TypeScript, interfaces can also extend classes, but only in a way that involves inheritance. When an interface extends a class, the interface includes all class members (public and private), but without the class’ implementations. In Figure 6, the ICustomer interface will have the private member id, public member Id and the public member MiddleName.
Figure 6 An Extended Class with All Members
The ICustomer interface has a significant restriction—you can only use it with classes that extend the same class the interface extended (in this case, that’s the Customer class). TypeScript requires that you include private members in the interface to be inherited from the class that the interface extends, instead of being reimplemented in the derived class. A new class that uses the ICustomer interface would need, for example, to provide an implementation for MiddleName (because it’s only specified in the interface). The developer using ICustomer could choose to either inherit or override public methods from the Customer class, but wouldn’t be able to override the private id member.
This example shows a class (called NewCustomer) that implements the ICustomer interface and extends the Customer class as required. In this example, NewCustomer inherits the implementation of Id from Customer and provides an implementation for MiddleName:
This combination of interfaces, classes, implementation and extension provides a controlled way for classes you define to extend classes defined in other object models (for more details, check out section 7.3 of the language specification, “Interfaces Extending Classes”). Coupled with the ability of TypeScript to use information about other JavaScript libraries, it lets you write TypeScript code that works with the objects defined in those libraries.
TypeScript Knows About Your Libraries
Besides knowing about the classes and interfaces defined in your application, you can provide TypeScript with information about other object libraries. That’s handled through the TypeScript declare keyword. This creates what the specification calls “ambient declarations.” You many never have to use the declare keyword yourself because you can find definition files for most JavaScript libraries on the DefinitelyTyped site at definitelytyped.org. Through these definition files, TypeScript can effectively “read the documentation” about the libraries with which you need to work.
“Reading the documentation,” of course, means you get data-typed IntelliSense support and compile-time checking when using the objects that make up the library. It also lets TypeScript, under certain circumstances, infer the type of a variable from the context in which it’s used. Thanks to the lib.d.ts definition file included with TypeScript, TypeScript assumes the variable anchor is of type HTMLAnchorElement in the following code:
The definition file specifies that’s the result returned by the createElement method when the method is passed the string “a.” Knowing anchor is an HTMLAnchorElement means TypeScript knows the anchor variable will support, for example, the addEventListener method.
The TypeScript data type inference also works with parameter types. For example, the addEventListener method accepts two parameters. The second is a function in which addEventListener passes an object of type PointerEvent. TypeScript knows that and supports accessing the cancelBubble property of the PointerEvent class within the function:
In the same way that lib.d.ts provides information about the HTML DOM, the definition files for other JavaScript provide similar functionality. After adding the backbone.d.ts file to my project, for example, I can declare a class that extends the Backbone Model class and implements my own interface with code like this:
Typescript Optional Abstract Property
If you’re interested in details on how to use TypeScript with Backbone and Knockout, check out my Practical TypeScript columns at bit.ly/1BRh8NJ. In the new year, I’ll be looking at the details of using TypeScript with Angular.
There’s more to TypeScript than you see here. TypeScript version 1.3 is slated to include union datatypes (to support, for example, functions that return a list of specific types) and tuples. The TypeScript team is working with other teams applying data typing to JavaScript (Flow and Angular) to ensure TypeScript will work with as broad a range of JavaScript libraries as possible.
If you need to do something that JavaScript supports and TypeScript won’t let you do, you can always integrate your JavaScript code because TypeScript is a superset of JavaScript. So the question remains—which of these languages would you prefer to use to write your client-side code?
Peter Vogelis a principal with PH&V Information Services, specializing in Web development with expertise in SOA, client-side development and UI design. PH&V clients include the Canadian Imperial Bank of Commerce, Volvo and Microsoft. He also teaches and writes courses for Learning Tree International and writes the Practical .NET column for VisualStudioMagazine.com.
Thanks to the following Microsoft technical expert for reviewing this article: Ryan Cavanaugh