Introduction to TypeScript: Type Annotations

14 August 2015, by

This is the 2nd post in our series on TypeScript. Take a look at the first post in this series for a bit more of an introduction to the basic of TypeScript, and the powers of type inference. In this post we’re going to take a more detailed look at the type annotations TypeScript provides to explicitly describe types, extending the power of static typing over more of our code that type inference can’t quite cover.

Extending types beyond pure inference

The simplest approach to typing your TypeScript code is to let type inference do it for you. This works well for locally defined and used variables, but falls down in cases where TypeScript can’t see enough context to know exactly what values we’re expecting. TypeScript infers types for variables by the values they’re initialised to, and by seeing them either returned by or provided as arguments to functions for which it already has types. That doesn’t cover many cases though, particularly the types of arguments in new function definitions, types for variables that aren’t immediately initialised, and any use of variables outside of the compiled TypeScript code (e.g. code coming from external JavaScript libraries).

This doesn’t necessarily result in a failure to compile your code. Variables that don’t have inferable types are given the ‘any’ type: a dynamic type that opts them out of type checking, and blindly trusts their usage. You can disable this by enabling the noImplicitAny) flag to require strict typing everywhere, but this is often useful behaviour initially; treating unknown variables as any allows you to gradually type a codebase, rather than forcing you to ensure everything is fully typed immediately. It’s rarely what you want in the long-term though. Types catch bugs, and the more specific you can be about the types you’re expecting, the more mistakes you’re going to catch at compile-time, before they hurt.

In these cases then where TypeScript can’t infer our types, how to we specify them? First the basics:

var x: string;

function aFunctionWithTypedArguments(a: number, b: Array<number>): void { ... }

function aFunctionReturningAnElement(): HTMLElement { ... }

function aGenericFunction<T>(arg: T): Array<T> { ... }

Here we annotate types on a variable, function arguments, function return types, and a generic function’s argument and return types. With these in place the compiler can now validate these types are correct (refusing to any attempts to call the 1st function with two numbers, for example), and can use these types in future inferences (for example automatically inferring the type of ‘c’ in var c = aFunctionReturningAnElement();).

Hopefully this is fairly intuitive to anybody who’s written code in a statically typed language before; all of this acts just as you’d expect coming from languages like Java or C#.

Note the generic code particularly. While this might look complex to anybody only familiar with JavaScript, it’s fundamentally the same as the generics used in throughout many popular statically typed languages. aGenericFunction here is a function that takes an argument of any type, and returns an array of that type: e.g. aGenericFunction(1) is guaranteed to return an array of numbers.

More complex type annotations

That’s it for the simple case. Unfortunately JavaScript has quite a few more complicated types than this though, and TypeScript aims to let you to describe all of the types that we see in real world JS. To do this TypeScript provides some more unusual types to effectively describe more complex structure:

  • Inline function types: var callback: (e: Event) => boolean

    JavaScript APIs tend to be very fond of passing functions around, particularly for callbacks, and the type system has to be able to keep up. The signatures for this is simple: brackets listing the argument types, and an arrow (=>) to the return type.

  • Anonymous object types: var x: { name: string };

    TypeScript has a structural type system: a variable matches a type if it has the same structure. X is matches type T if X has all the properties that T has, with the same types. You can think of this as compile-time duck typing. This differs drastically from languages like C# or Java with nominal type systems, where types match only if there’s an explicit relationship between them (e.g. one one is a subclass of the other, or explicitly implements its interface).The end result is that you can define types by their structure alone. Above for example is a variable that can be assigned any object with a name property that’s a string. TypeScript will then allow you to set it to any kind of object from any source, as long as it fulfills that description, and catch any cases that don’t fit that at compile time. This is a key, as lots of existing JavaScript depends on duck-typing, and would be extremely difficult to externally type with a more traditional OO type system.

  • Union types: var x: string|number

    Union types are a fairly new TypeScript feature, added in 1.4. They allow you to describe a variable as either being of type A or type B, and will only allow you to perform operations valid on both. To then pick a specific one of the two options you can use type guards: if (x instanceof string) { ... }. TypeScript’s inference engine understands type guard expressions like these, and will allow you to use the variable as the specific type that you’ve checked for within the body of the if block.In addition TypeScript also has explicit casting, like many statically typed languages, for cases where you want to tell the compiler you’re already sure what type something is (var y = <number> x;).Like structural typing, union types are useful because they closely match common patterns used in existing JavaScript code. Many libraries (e.g. JQuery) return completely different types of variable depending on the specific arguments provided at runtime, and this provides a very effective way of explicitly describing and handling that case.

Defining your own types

That’s the essence of how you annotate your types in TypeScript. With this, you can add annotations describing the core structure and contracts with your code, to get type checking across your codebase.

This is still a bit limited though: we can only use built in types (number, string, HTMLElement), or combinations and structures we build from those explicitly. In the next post in this series we’ll take a closer look at that, and the tools TypeScript provides to let you define your own types, with classes, enums, and more.

Tags: , , , ,

Categories: Technical


Comments are closed.