Introduction to TypeScript: Type Declarations


19 August 2015, by

This is the 3rd post in our series on TypeScript. Take a look at the first and second in this series for an introduction to the basic of TypeScript, type inference, and type annotations. In this post we’re going to take a more detailed look at how we can define our own types in TypeScript, to be used by the inference engine and in our own annotations.

Defining your own types

Up until now we’ve looked at ways to describe types in terms of the predefined types available (e.g. string, number or HTMLElement). This is powerful alone, but defining our own types on top of this dramatically increases its usefulness. In TypeScript there are a few main ways to define a new type:

    • Type aliases: type ElementGenerator = (a: string, b: string, c: boolean) => HTMLElement

      The easiest option is to just define a new simpler name for an existing type. Type aliases (added in 1.4) let you do this. This new type can then be used exactly as you would any other type, but with a shorter easier snappier name.

      Note that this is mostly just adding readability to your code: unlike equivalents in other languages (such as Haskell’s newtype) structural typing means this doesn’t increase type safety. Since types are still matched only structurally you can’t define type minutes = number, and use that to check that your variable is set only with values that are explicitly specified as being of the minutes type; any number is always a valid minute.

    • Interfaces
      interface MyInterface {
        property1: number;
        anotherProperty: boolean[];
        aMethod(HTMLElement): boolean;
        eventListener: (e: Event) => void;  
      }
      
      // Interfaces can be extended, as in most other modern OO languages
      interface My2ndInterface extends MyInterface {
        yetAnotherProperty: HTMLElement;
      }
      
      // Similarly, interfaces can be generic as in other languages
      interface MyGenericInterface<T> {
        getValue(): T;
      }
      
      interface FunctionInterface {
        // Objects fulfilling this are callable functions, taking a number and returning a boolean
        (x: number): boolean;
      
        // We can also have hybrid interfaces: functions that also have properties (like jQuery's $)
        myProperty: string;
      }
      
      interface DictionaryInterface {
        // Objects fulfilling this act as dictionaries, with arbitrary string keys and numeric values
        [key: string]: number;   
      }
      

      Interfaces act mostly as you’d expect from languages like Java, C# and Swift, with a few extra features. Any function or variable that is annotated or inferred to have the type of the interface will only accept values that match this, giving you guarantees that your values are always what you expect. Note too that all of this is just for compile-time checking; this is all thrown away by the compiler after compilation, and your interfaces don’t existing in the resulting compiled JS.

      The key major difference between how this works and most other languages is that this type checking is done purely structurally. A value matches an interface only because they have the same shape, not because they’re explicitly indicated as being a member of that that interface anywhere else.

    • Classes
      class MyClass extends MySuperclass implements AnInterface {
        // Fields and methods can have private/public modifiers (they're otherwise public by default)
        private myField: number;
      
        constructor(input: number) {
          super("hi"); // Subclasses have to call superclasses constructors before initializing themselves
          this.myField = input * 2;
        }
      
        calculateAnImportantValue(newInput: number): number {
          return this.myField * newInput + 1;
        }
      
        // Classes can include property accessors
        get propertyAccessor(): number {
          return this.myField;
        }
      
        static myStaticFunction(xs: number[]): MyClass[] {
          return xs.map(function (x) {
            return new MyClass(x);
          });
        }
      }
      
      // Classes are instantiated exactly as in vanilla JavaScript.
      // (The type alias here is just for clarity; it'd be inferred anyway)
      
      var instance: MyClass = new MyClass(10);
      

      Classes simultaneously define two things: a new type (the corresponding interface for the class), and an implementation of that type. The actual implementation of this acts the same as the new built-in class functionality in ES2015 (ES6) – defining a constructor function and attaching methods to the prototypes – but

      You are only allowed one implementation per function in Typescript (including the methods and constructor here) to ensure compatibility when using the compiled code from pure JavaScript, so there’s no method overloading like you might’ve seen elsewhere. You can have multiple type definitions for a function though to simulate this yourself, although it’s a bit fiddly. Take a look at function overloads in the TypeScript handbook for the gory details.

    • Enums
      enum Color {
        Red,
        Green,
        Blue
      }
      
      var c: Color = Color.Blue;
      

      Another nice feature, straight from the standard OO playbook. Enums let you define a fixed set of values with usable friendly names (underneath they’re each just numbers, and an object to let you look up numbers/names from one another).

      Structural typing here is actually something of a hinderance however, limiting the value of enums compared to elsewhere. Enums become far more powerful within nominal type systems, whereas in TypeScript you sadly can’t check a method that takes a Color from above isn’t given any old potentially invalid number instead, for example. Take a look at the TypeScript playground at http://goo.gl/FZMzaj for a happily compiling but totally incorrect example.

      Nonetheless, while enum’s safety-giving power is limited they can still bring quite a bit of clarity and simplicity to code, and are definitely a useful tool regardless.

Describing external types

Sometimes you want to use TypeScript code that you didn’t write, but you’d still like it to be safely typed. Fortunately TypeScript supports exactly that.

Using the above type definitions we can describe the full shape of an external library totally externally to it, without having to actually change the code. Structural typing means the original library code doesn’t need to specify which interfaces it supports, and we just need a definition of the interface of the library, and to tell TypeScript that a variable matching this interface is available.

To do that, we use ambient modules; these describe modules of code that are defined outside our codebase. They can be either ‘internal’ (the variables declared are already defined in the global scope, typically by a script tag somewhere) or ‘external’ (the variables declared are exposed by a library that needs to be loaded through a module loader like CommonJS or AMD – we’ll look at TypeScript modules in a later post).

This is all very interesting, but helpfully you don’t really need to go any deeper than that for now. The syntax for this isn’t particularly important for TypeScript development day-to-day (although the section in the TypeScript handbook includes a few illustrative examples), because it’s already been done for you, for almost library you’ll use, as part of a project called DefinitelyTyped.

DefinitelyTyped includes not only type definitions for every library you might want (e.g. jQuery, lodash or loglevel), but also its own package manager TSD, to automatically retrieve and updates these type definitions for you.

To get started with this, just install TSD (npm install tsd -g), install the type definitions you need (tsd install jquery knockout moment --save), and ensure they’re referenced in your compilation process (e.g. include them as files to compile on the command line to tsc, add them to your input files list in grunt-ts, or use <reference> tags). TypeScript will know the variables exposed by each library and their types, giving you strong static typing wherever they’re used.

Bonus TypeScript Features

With this 3rd post, you’ve now seen the core of everything TypeScript has to offer, when automatically inferring types, manually annotating types yourself, and defining your own types to extend inference and annotation even further.

That’s not all TypeScript has to offer though. On top of this, TypeScript adds a selection of interesting bonus features, drawn from both ES2015 (ES6) and other languages, but compiling down into backward-compatible JavaScript you can run anywhere. Watch this space for the 4th and final post in this series, where we’ll take a closer look at exactly what’s available there, and how you can put it to use.

Tags: , , , ,

Categories: Technical

«
»

Comments are closed.