Get the JS Tips Newsletter!

From JavaScript to TypeScript, Pt. III: Type Inference & Compatibility

Peleke Sengstacke
๐Ÿ‘๏ธ 3,633 views
๐Ÿ’ฌ comments

Considering it's called TypeScript, it's probably no surprise that there's more to types than annotating a variable as a string. The compiler does a lot of work under the hood to put those types to work, and figure out what they should be even when you leave the annotations out.

This article will explore how TypeScript determines:

  1. The types of varibles you choose not annotate; and
  2. Whether assignment statements are safe -- that is, if the value you're assigning "fits" into the type of the variable you're trying to put it in.

I'm assuming you're using Node to run the sample code; I'm running v6.0.0. To get the code, clone my repo, and checkout the Part_3 branch:

Table of Contents

    git clone
    git checkout Part_3-Type_Inference_and_Compatibility

    You can compile all the .ts files with tsc -t ES5, which will put the compiled Javascript in a folder called built. It'll also produce an unsettling litany of errors, but you can ignore them.

    Type Inference

    TypeScript doesn't force you to annotate types in your code. With few exceptions, vanilla ES6 is perfectly valid. Even so, TypeScript will always try to figure out the types of unannotated variables , so you get the benefits of type-checking without the hassle of having to do it yourself.

    // basic_inference.ts
    "use strict";
    // Requires string inputs
    function buildName (first_name : string, last_name : string, title : string) : void {
        if (title)
            return `${title} ${first_name} ${last_name}`
            return `${first_name} ${last_name}`
    // No annotations . . . 
    const first_name = l337,
         last_name = 'Haxor',
         title             = 'Dr';
    // . . . But TypeScript's a smart language that don't need no annotations
    console.log(buildName(first_name, last_name, title)); // TypeError

    If you run the code snippet above, you'll get a type error, even though there's not an annotation in sight. Type Inference is the process of determining an unannotated variable's type, and this is it in action.

    Example of TypeError thrown by unannotated variable

    The rule for primitive types is that TypeScript knows them when it seems them: You don't need to annotate these.

    "use strict";
    function queryUrl( url : string ) : any {
        let result;
        // Do network magic
        return result;
    // All TypeErrors.
    let url = 42;
    url = { url : '' };
    url = false;

    It's with collections, classes, and interfaces that things get hairy.

    Union Types

    TypeScript uses union types in more complicated cases.

    A union type is somewhere between a specific type and any. A union type gives a list of types that a variable can contain, or that a value can have .

    You denote a union type as a parenthesized list of types separated by a pipe. A union type satisfied by either a string or a number looks like: (string | number).

    // union.ts
    "use strict";
    class Article {
        static articles : Article[] = [];
        // Function overload
        static submit (article) : boolean;
        static submit (article) : Article;
        static submit (article  : Article) : any {
            if (Article.articles.length >= 1)
                return article;
            else {
          return true;
        constructor (public text : string) { }
    // Submit returns true if the submission was successful, and the
    //    article that failed to be submitted, otherwise.
    const bool : (boolean | Article) = Article.submit(new Article("It was the best of times . . . ")), // true
          obj  : (boolean | Article) = Article.submit(new Article("Wasn't my best article, anyway . . . ")); // article
    console.log(bool); // true
    console.log(obj); // Article { text : 'Wasn\t my best article, anyway . . . ' }

    A union is useful for enumerating options in situations where you expect one of a set of well-defined types to come through, but can't be sure of exactly which. Common examples of this include:

    1. Variables receiving return values from functions that return values of different types for the same input depending on state (submit); and
    2. Setting the type for a collection, such as an array, which may naturally contain a well-defined set of different types.

    Speaking of collections . . .

    Best Common Type

    Untyped Maps and Sets automatically have the type any:

    "use strict";
    const map = new Map(),
        set    = new Set();
    // No trouble with either of these . . .
    map.set('1', 'spitting'); // String key, String value
    map.set(1, 'game'); // number key, String value
    // . . . Nor any of these.
    set.add(2718); // number
    set.add('Euler\'s Constant'); // String
    set.add(true); // boolean

    The algorithm for determining the right type for an array of arbitrarily typed elements is more complicated.

    Inferring Types for Arrays of Primitives

    There are two possibilities for unannotated arrays of primitives.

    1. The array contains values of only one type. If the array contains values of type T, the array will have type T[].
    2. The array contains values of multiple types . If the array contains several types, TypeScript assigns a union type over all of them.

    The short version is this: If you initialize your array with . . .

    • No elements, it will have type any[]. You can add whatever you like to it later.
    • Elements of one type, T, it will have type T[]. You can only add values of type T to it ater.
    • Elements of several primitive types -- say, A, B, and C -- you can add any value of type A, B, or C to it later. It will have the union type (A | B | C)[].
    // collection_inference.ts
    "use strict";
    const something_and_nothing = [99, 100];
    // These all work.
    const names_and_numbers = ['Adonis', 'Aphrodite', 12];
    // This throws, because a Boolean is neither a String nor a Number.

    If you try this, you'll get an error, because true -- a boolean value -- doesn't belong in an array of type string | number.

    boolean not assignable to array for strings and numbers.

    What happened here?

    1. We declared an untyped array, called names_and_numbers, and tossed some values inside.
    2. Then, TypeScript looked at every value inside of it, and created a set of the types it found: In this case, string and number.
    3. Finally, TypeScript selects the type in the set that satisfies every value. In other words, it assigns the array the best common type -- ideally, the supertype of all the values.

    When TS meets the first array, it sees that every value is a number, and types the list accordingly. We can add 101 without trouble because it's a number. null and undefined are allowed, because they're subtypes of every type.

    "null is considered a valid value for all primitive types, object types, union types, intersection types, and type parameters, including even the Number and Boolean primitive types . . . [and] undefined is considered a valid value for all primitive types, object types, union types, intersection types, and type parameters." ~ TypeScript Specification, ยง3.2.6-7

    TypeScript does the same for the second list. It walks through the array; creates a set of the types it finds (string, number); and chooses the supertype.

    . . . But there is no supertype, because a number isn't a string, and a string isn't a number, In this case, TypeScript realizes that each element is either a string or a number, and assigns a union type, (string | number), instead.

    Inferring Types for Arrays of Objects

    For the most part, inferring the type of an array containing objects is a symmetrical problem. Most of the type inferences you'll deal with fall into one of two categories:

    1. The objects share a nominal superclass , but no instance of the superclass is in the array . This results in a union type.
    2. The objects share a nominal superclass , and an instance of the superclass is in the array . In this case, the array will have the superclass type.

    There are unintuitive edge cases here, so the best approach is to always type your object arrays.

    The Case of the Common Superclass . . . Or Not

    Consider the two arrays at the bottom.

    // hierarchical_inference.ts
    "use strict";
    // This is a textbook example of a senseless class hierarchy: This should be an interface. 
    //   But  you can't instantiate an interface, so I'm using a class for illustration.
    class Named { name : string; }
    "use strict";
    class Nameable {
      constructor (public name : string) { }
    class Person extends Nameable {
      private boolean : married;
      constructor (name : string) { super(name) }
    class Museum extends Nameable {
      private open : boolean;
      constructor (name : string) { super(name) }
    class Planet extends Nameable {
      private habitable : boolean;
      constructor (name : string) { super(name) }
        // (Museum | Planet)[]
    const union   = [new Museum('MOMA'), new Planet('Zeltron')],
        // Nameable[]
           proper  = [new Person('Gauss'), new Museum('Met'), new Planet('Xaltrix'), new Nameable()];

    Here, union has type (Museum | Planet)[], but Named[] is the better choice.

    This is because, when TypeScript selects a best common type from the set of the types in a given array, it can only choose from the types actually in the array . Since TypeScript doesn't find an instance of Named in union, it can't figure out that it's the correct superclass. Instead, it settles for the next best thing: A union type acounting for whatever classes it does find.

    TypeScript gets it right with proper, however, since this array does contains an instance of Named. Since it's in the set of possible types that TS can choose from, TypeScript identifies that it's the superclass of all the other types in the array, and types it accordingly.

    Again, best practice is to type arrays containing class instances. TypeScript can't always make the "obvious" choice when dealing with user-defined hierarchies.

    Type Compatibility

    Let's say I have an object, and I want to stuff it in a variable of type T. TypeScript only lets me do that if the type of the value is compatible with the type of the variable. The notion of a value being compatible with the type of its container is called type compatibility .

    The obvious case is when the object is of the same type as the variable. If I try to shove a Person value into a variable of type Person, things work fine.

    It gets more interesting when you try to assign a value to a variable of a different type. TypeScript rejects this outright with primitives -- assigning a number to a string variable is always wrong. But, it's flexible when it comes to assigning instances of a class or interface.

    Structural Typing

    Structural typing is a way to determine if types are compatible based on their shapes, rather than their hierarchical relationships.

    In traditional OOP, otherwise identical objects from different class hierarchies can't be treated as equivalent; what matters is not their interfaces or data members, but their f nominal class hierarchies . As we'll see, TypeScript is more flexible than this.

    Class Compatibility

    An object's structure is effectively the list of its data members . If two objects share the same data members, they're structurally equivalent.

    // compatibility.ts
    "use strict";
    // Structurally identical classes are equivalent.
    //   This seems obvious, but would not be the case in a classical OO language.
    class Book {
      constructor (public title : string, 
                   public length : number, // Printable pages
                   public author : string) { }
    class Article {
      constructor (public title : string,
                   public length : number,
                   public author : string) { }
    // Structural identity
    let x : Book;
    x = new Article('From JavaScript to TypeScript III', 6, 'Peleke')

    Two different classes that are structurally identical are compatible. This seems self-evident if you have a JavaScript background, but is disallowed in more traditional languages.

    There's one exception to this rule: Objects with private and protected members can't be compatible with objects from different classes , even if they have the same shape.

    // compatibility_private_members.ts
    "use strict";
    // Identical shapes and member names, but incompatible due to private members.
    class User {
        constructor (private name : string, private age : number) { }
    class Country {
        constructor (private name : string, private age : number) { }
    let rando : User;
    rando = new Country('Azerbaijan', 15); // Error

    This won't compile, because name is private in both Country and User. This restriction is in place to enforce the fact that private members should be exposed only to instances of the owning class.

    Separate declarations of private property error

    Finally, be aware that static members on a class don't matter where type compatibility is concerned. TpeScripty only cares about instance-level members.

    // compatibility_static_members.ts
    "use strict";
    // Identical shapes and member names, but incompatible due to public members.
    class User {
      static users : User[];
        static addUser (user : User) : void {
          if (user)
        constructor (public name : string, public age : number) { }
    class Country {
      static COUNTRIES : number = 197;
      static getCountryCount () : string {
        return `There are ${197} countries.`;
        constructor (public name : string, public age : number) { }
    // These classes have completely different static properties and methods,
    //   but their instances have the same shape, so they're compatible.
    let rando : User;
    rando = new Country('Azerbaijan', 15); // Works fine

    This behaves as it does because static properties are defined on the class's constructor function itself, so instances can't access it via delegation. That means it doesn't influence their shape, and so shouldn't influence any checks for structural equivalence.

    Interface Compatibility

    Compatibility works much the same for interfaces, without the caveats regarding static members and private/protected members.

    // compatibility.ts
    "use strict";
    interface Printable {
      print () : void;
    class Book {
      constructor (public title : string, 
                   public length : number, // Printable pages
                   public author : string,
                   public text : string) { }
      print () : void {
    // Compatibile, because Book has all of the members that Printable does.
    let printable : Printable
    printable = new Book('Eugene Onegin', 132, 'Alexander Pushkin', 'Not planning fun . . . ');
    // We can also cast between compatible types without error.
    const book : Book = new Book('1984', 222, 'Orwell', 'Perhaps one did not want to be love so much as understood.'),
      new_printable = <Printable> book;
    // This doesn't work with incompatible types!
    //  If you run this line, you'll get an error, because the object doesn't
    //  contain a member called print.
    // ==
    // const broken = <Printable> { not_printable : 'Well, this was a mistake.' }

    The sole criterion for Book to be structurally compatible with Printable is that it implement a void method called print. Since it does, they're compatible. It doesn't matter that Book has more members besides. This is true of class instances, as well.

    Comparing Functions

    Since we can pass functions around like anything else, TypeScript has rules for the legality of assigning function values. They check two things:

    • Parameter lists ; and
    • Return types .

    Parameter Compatibility

    You can assign a function to a type if the type takes the same types of arguments, in the same order, even if the type you're assigning to expects more parameters.

    // function_compatibility.ts
    "use strict";
    let buildName = function buildName ( first_name : string, last_name : string) : string {
        return `${first_name} ${last_name}`;
    let fetchData = function fetchData ( url : string ) : string {
        return `Fake dat from ${url}`;
    let build = buildName, // (( first_name : string, last_name : string) => string)
        fetch = fetchData; // (( url : string) => string)
    // Compatible, because fetchData's parameter list is a subset of buildName's.
    build = fetchData; // LINE A
    // No-go, because buildName requires a second string argument, which fetch_data doesn't support.
    // fetch = buildName;

    In Line A, fetchData -- the function whose value we're assigning -- is in what's called the source position. build is in what's called the target position.

    Throw awaying arguments like that might feel strange, but we actually do it all of the time.

    "use strict";
    const users = [
        { name : 'Horus', symbol : 'wedjat eye' },
        { name : 'Set', symbol: 'was-sceptre' },
        { name : 'Neuth', symbol : 'stars' }
    // JS passes map the current index and the whole array
    //   on each iteration, but we generally throw both away.
    const names = => };

    If we call a function with more arguments than it expects, it ignores everything but what it expected. This is why we can assign a function (let's call it fun) that takes fewer arguments to a value typed to take more. If we pass extra arguments, fun just ignore them.

    TypeScript won't let us go the other way, though, because a function expecting two arguments presumably can't do its job without both of them.

    This is actually directly analogous to sub- and superclass relationships with objects. If a function of type f can "hold" a more specific function of type g, f is a supertype of g. It's like saying that an instance of the User base class can contain an instance of a more specific child class, like LoggedInUser. . . But functions are cooler and more abstract.

    Just two notes to wrap up:

    1. [Rest parameters]() don't influence this check. If two functions have compatible parameter lists, and one of them has rest params, they're still considered compatible.
    2. Two functions can only be compatible if the return type of the source function is a subtype of the target function's return type.

    Function Arrays

    I'll hazard a guess that you probably won't create a ton of function arrays in your lifetime. I've come across it when working with Rx.js, though, so it's worth mentioning.

    There are two common cases -- arrays of:

    1. Functions with completely different signatures . In this case, TypeScript assigns the array a union type covering all of the function types.
    2. Functions with compatible signatures . In this case, TypeScript assigns the array the most general type compatible with the others.

    These two can occur together, as well, which we'll see in the example.

    Unions Over Function Types

    If the functions in your array have completely different parameter lists and return types, TypeScript assigns the array a union type covering all of them.

    // function_array_union.ts
    "use strict";
    const function_array = [
      // ((bar : string) => void)
      function foo (bar : string) : void { console.log(bar) },
      // ((answer : number) => string)
      function baz (answer : number) : string { 
          return `The answer to life is ${answer}.`;

    The types of these functions don't intersect at all, so TypeScript assigns the array the union type ((bar : string) => void) | ((answer : number) => string).

    Arrays with Compatible Functions

    If one of your functions is compatible with the others, TypeScript will assign the array the most general function type.

    // function_array_compatible.ts
    "use strict";
    class User {
      constructor (public username : string, public email : string) { }
    class LoggedInUser extends User {
      public logged_in : boolean = true;
      constructor (username : string, email : string) { super(username, email) }
      logout () : void {
        // Logout logic
    const function_array = [
      // ((user : LoggedInUser) => void)
      function logout(user : LoggedInUser) : void { user.logout() },
      // ((user : User, email : string) => void)
      function setEmail (user : User, email : string) : void { = email;

    In this case, function_array has the type ((user : User => void, email : string => void) => void)[]. This is essentially because setEmail's type is compatible with logout, but not vice-versa.

    Finally, keep in mind that both of these things can happen at once. If one function in the array is a supertype of some others, but there are black sheeps that just don't fit in, TypeScript will create a union type of the supertype and stragglers.

    // function_array_compatible.ts, bottom
    // Dummy class to pleae compiler
    class Friend { }
    const function_array2 = [
      // ((user : LoggedInUser) => void)
      function logout(user : LoggedInUser) : void { user.logout() },
      // ((user : User, email : string) => void)
      function setEmail (user : User, email : string) : void { = email;
      // ((user : User, amount : number) => void)
      function chargeAccount ( user : User, amount : number) : void {
        // Take user's money to fund more features

    In this case, function_array2 will have the gnarly type (((user: User, email: string) => void) | ((user: User, amount: number) => void))[]. This is just a union type covering the type we had in the last section, along with the type of chargeUser.

    Again, these rules are no different from those in place when dealing with arrays of objects. All we've done is generalize the notion of supertype according to TypeScript's rules of function compatibility.

    Don't sweat it if this is a bit alien, though. I've had to know these rules before, but it only comes up every once in awhile.


    There's more to type compatibility, and the spec contains the exhaustive details.

    I find that details beyond these are mostly of theoretical interest, though: This is the 80/20 of what I've needed to understand when debugging sloppily typed code or dealing with generics, which we'll take a close look at next time.

    As always, feel free to leave questions in the comments, or shoot them to me on Twitter (@PelekeS)!

    Peleke Sengstacke

    13 posts

    Peleke Sengstacke is a web and Android developer with a soft spot for functional programming.

    He likes linguistics, Haskell, and powerlifting. He dislikes mosquitoes, cramped bus rides , and merge conflicts.

    Catch him on Twitter (@PelekeS), or sign up for his email list on what to learn and where to learn it ( It's wicked educational.