CHAPTER 2

image

The Type System

The fundamental problem addressed by a type theory is to insure that programs have meaning. The fundamental problem caused by a type theory is that meaningful programs may not have meanings ascribed to them. The quest for richer type systems results from this tension.

—Mark Manasse

In this chapter you will learn about the TypeScript type system, including some of the important ways in which it differs from other type systems that you may have encountered before. As TypeScript has drawn inspiration from a range of languages it is worth understanding the subtle details because leaning on your existing knowledge of other type systems may lead to some surprises. These details are explored by comparing structural and nominal type systems and by looking at the details of optional static types, type erasure, and the powerful type inference provided by the TypeScript language service.

At the end of the chapter is a section about ambient declarations, which can be used to fill in type information for code that hasn’t been written in TypeScript. This allows you to consume external code with type checking and autocompletion, whether it is old JavaScript code you already have, additions to the runtime platform, or third party libraries and frameworks that you use within your program.

Type Systems

Type systems originate from type theory, which is credited to Bertrand Russell who developed the theory in the early 20th century and included it in his three volume Principia Mathematica (Whitehead and Russell, Cambridge University Press, 1910). Type theory is a system in which each term is given a type and operations are restricted based on the types. TypeScript in particular, with its type annotations, has a style reminiscent of the building blocks of type theory, as shown in Figure 2-1.

9781430267911_Fig02-01.jpg

Figure 2-1. Type theory and TypeScript similarities

In type theory, a symbol is annotated with a type just like with TypeScript type annotation. The only difference in this respect is that type theory leaves out the var keyword and uses the nat type (a natural number) rather than the number type in TypeScript. Function annotations are also recognizable, with type theory leaving out the parentheses.

In general, a type system assigns a type to each variable, expression, object, function, class, or module in the system. These types are used alongside a set of rules designed to expose errors in the program. These checks can be performed at compile time (static checking) or at runtime (dynamic checking). Typical rules would include ensuring that the value used in an assignment is the same type as the variable it is being assigned to, or ensuring that a function call supplies arguments of the correct type based on the function signature.

All of the types used within a type system act as contracts that state the accepted interactions between all of the various components in the system. The kinds of errors that are detected based on these types are dependent on the rules in the type system and the level of complexity in the checking.

Optional Static Types

JavaScript is dynamically typed; variables do not have an associated type, so no type restrictions can be applied to operations. You can assign a value of one type to a variable and later assign a value of a completely different type to the same variable. You can perform an operation with two incompatible values and get unpredictable results. If you call a function, there is nothing to enforce that you pass arguments of the correct type and you can even supply too many or too few arguments. All of these are demonstrated in Listing 2-1.

Listing 2-1. JavaScript dynamic types

// Assignment of different types
var dynamic = 'A string';

dynamic = 52;

// Operations with different types
var days = '7';
var hours = 24;

// 168 (luckily, the hours string is coerced)
var week = days * hours;

// 77 (concatenate 7 and 7)
var fortnight = days + days;

// Calling functions
function getVolume(width, height, depth) {
        return width * height * depth;
}

// NaN (10 * undefined * undefined)
var volumeA = getVolume(10);

// 32 (the 8 is ignored)
var volumeB = getVolume(2, 4, 4, 8);

The JavaScript type system is incredibly flexible because of this, but sometimes this flexibility can cause problems.

TypeScript provides a system for inferring and specifying types, but allows types to be optional. The optionality is important because it means you can choose when to enforce types and when to allow dynamic types. Unless you opt out of type checking, using the any keyword, TypeScript will attempt to determine the types in your program and will check inferred types and explicit types you specify using type annotations. Type annotations are described in Chapter 1.

All of the checks are performed at compile time, which is what makes TypeScript statically typed. The compiler is responsible for constructing a schedule of all of the types, checking all expressions against the types and removing all of the type information when it converts the code into valid JavaScript.

If you were to paste the JavaScript code from Listing 2-1 into a TypeScript file, you would receive errors for all of the type errors found in the example. You can see the errors being flagged in the TypeScript listing in Figure 2-2 below.

9781430267911_Fig02-02.jpg

Figure 2-2. TypeScript compiler errors

Image Note  One of the key points in TypeScript’s type system is the optionality of the types. This effectively means you are not restricted to static types and can opt to use dynamic behavior whenever needed.

Structural Typing

TypeScript is a structural type system; this contrasts with most C-like languages, which are typically nominative. A nominative, or nominal, type system relies on explicit named annotations to determine types. In a nominal system, a class would only be seen to implement an interface if it was decorated with the name of the interface (i.e., it must explicitly state that it implements the interface). In a structural type system, the explicit decoration is not required and a value is acceptable as long as its structure matches the specification of the required type.

A nominal type system is intended to prevent accidental type equivalence—just because something has the same properties does not mean it is valid—but as TypeScript is structurally typed accidental type equivalence is possible.

In a nominal type system you could use named types to ensure that correct arguments were being passed, for example, you could create a CustomerId type to wrap the identifier’s value and use it to prevent assignment of a plain number, or ProductId, CustomerTypeId, or any other type. A type with identical properties, but a different name, would not be accepted. In a structural type system, if the CustomerId wrapped a public property named value that contained the ID number, any other type that had a value property with an equivalent type would be acceptable.

If you wanted to use custom types for this kind of type safety in TypeScript, you would have to ensure the types were not accidentally equivalent by making them structurally unique. You could do this by adding a public property with a unique name (such as customerIdValue on a CustomerId interface), or by adding a private member to a class. Whenever you add a private member to a class, it becomes impossible to match its structure. If you require the different ID objects to be polymorphic, you could achieve this by having an additional property or method that would have the same name on all of the objects.

In Listing 2-2 the CustomerId class uses the getValue method wherever it is needed to be substituted for ObjectId and the private id  property avoids accidental equivalence.

Listing 2-2. Using and avoiding equivalence

interface ObjectId {
   getValue(): number;

}

class CustomerId {
    constructor(private id: number) {
    }
    getValue() {
        return this.id;
    }
}
class ProductId {
    constructor(private id: number) {
     }
    getValue() {
        return this.id;
    }
}
class Example {
    static avoidAccidentalEquivalence(id: CustomerId) {
        // Implementation
    }
    static useEquivalence(id: ObjectId) {
        // Implementation
    }
}
var customerId = new CustomerId(1);
var productId = new ProductId(5);

// Allowed
Example.avoidAccidentalEquivalence(customerId);

// Errors 'Supplied parameters do not match signature of call target'
Example.avoidAccidentalEquivalence(productId);

// Allowed
Example.useEquivalence(customerId);

// Allowed
Example.useEquivalence(productId);

While structural typing may seem to cause difficulties in some situations, it has many advantages. For example, it is far easier to introduce compatible types without having to change existing code and it is possible to create types that can be passed to external code without inheriting from an external class. It is also impossible to create a new supertype in a nominally typed language without changing all of the subtypes, whereas in a structurally typed language you can easily create new supertypes without making changes elsewhere in your program.

One of the most significant benefits of structural typing is it saves myriad explicit type name decorations. In Listing 2-2 neither the CustomerId or ProductId classes have the interface declaration: implements ObjectId, but they can both be passed to the useEquivalence method because they are compatible with the ObjectIdthey have the same structure. To satisfy a type requirement, the type must have matching properties and methods. The properties and methods should all be of the same type, or compatible types. A compatible type can be a subtype, a narrower type or a structurally similar type.

One thing to avoid in a structurally typed language, such as TypeScript, is empty structures. An empty interface or an empty class is essentially a valid supertype of practically everything in your program, which means any object could be substituted for the empty structure at compile time because there is no structure that the object would have to satisfy during type checking.

Structural typing complements the type inference in TypeScript. With these features, you can leave much of the work to the compiler and language service, rather than having to explicitly add type information and class heritage throughout your program.

Image Note  You can’t rely on named types to create restrictions in a TypeScript program, only unique structures. For interfaces, this means creating uniqueness using a uniquely named property or method. For classes, any private member will make the structure unique.

Type Erasure

When you compile your TypeScript program into plain JavaScript, the generated code is different in two ways: code transformation and type erasure. The code transformation converts language features that are not available in JavaScript into representations that are valid. For example, if you are targeting ECMAScript 5, where classes are not available, all of your classes will be converted into JavaScript functions that create appropriate representations using the prototypal inheritance available in ECMAScript 5. Type erasure is the process that removes all of the type annotations from your code, as they are not understood by JavaScript.

Type erasure removes both type annotations and interfaces. These are only required at design time and at compile time for the purpose of static checking. At runtime types are not checked, but you shouldn’t encounter a problem because they have already been checked when you compiled your program (unless you used the any type).

Listing 2-3 shows an example TypeScript listing for an OrderedArray class. The class is generic, so the type of the elements in the array can be substituted. For complex types, an optional custom comparer can be supplied to evaluate the items in the array for the purposes of ordering, but for simple types it can be omitted. Following the class is a simple demonstration of the class in action. This code compiled into the JavaScript shown in Listing 2-4. In the compiled output all of the type information is gone and the class has been transformed into a common JavaScript pattern called a self-executing anonymous function.

Listing 2-3. TypeScript ordered array class

class OrderedArray<T> {
    private items: T[] = [];

    constructor(private comparer?: (a: T, b: T) => number) {
    }

    add(item: T): void {
        this.items.push(item);
        this.items.sort(this.comparer);
    }

    getItem(index: number) : T {
        if (this.items.length > index) {
            return this.items[index];
        }
        return null;
    }
}

var orderedArray: OrderedArray<number> = new OrderedArray<number>();

orderedArray.add(5);
orderedArray.add(1);
orderedArray.add(3);

var firstItem: number = orderedArray.getItem(0);

alert(firstItem); // 1

Listing 2-4. Compiled JavaScript code

var OrderedArray = (function () {
    function OrderedArray(comparer) {
        this.comparer = comparer;
        this.items = [];
    }

    OrderedArray.prototype.add = function (item) {
        this.items.push(item);
        this.items.sort(this.comparer);
    };

    OrderedArray.prototype.getItem = function (index) {
        if (this.items.length > index) {
            return this.items[index];
        }
        return null;
    };
    return OrderedArray;
})();

var orderedArray = new OrderedArray();

orderedArray.add(5);
orderedArray.add(1);
orderedArray.add(3);

var firstItem = orderedArray.getItem(0);

alert(firstItem); // 1

Despite the type erasure and transformations performed during compilation, the JavaScript output is remarkably similar to the original TypeScript program. Almost all transformations from TypeScript to JavaScript are similarly considerate of your original code. Depending on the version of ECMAScript you are targeting, there may be more or fewer transformations, for example, the ECMAScript 6 features that TypeScript transforms for compatibility with ECMAScript 3 and 5 wouldn’t need to be transformed if you were to target ECMAScript 6.

Type Inference

Type inference is the polar opposite of type erasure. Type inference is the process by which types are determined at compile time in the absence of explicit type annotations.

Most basic examples of type inference, including the early examples in this book, show a simple assignment and explain how the type of the variable on the left of the assignment can be automatically set to the type of the value on the right hand side. This kind of type inference is really level one for TypeScript, and it is capable of some incredibly complex determinations of the types in use.

TypeScript performs deep inspections to create a schedule of types in your program and compares assignments, expressions, and operations using this schedule of types. During this process, there are some clever tricks that are employed when a direct type is not available, which allow the type to be found indirectly. One such trick is contextual typing, where TypeScript uses the context of an expression to determine the types.

Listing 2-5 shows how types can be inferred in progressively more indirect ways. The return value of the add function is determined by working backward from the return statement. The type of the return statement is found by evaluating the type of the expression a + b, which in turn is done by inspecting the types of the individual parameters.

In the very last expression in Listing 2-5, the result parameter type in the anonymous function can be inferred using the context that the function is declared in. Because it is declared to be used by the execution of callsFunction, the compiler can see that it is going to be passed a string, therefore the result parameter will always be a string type. A third example comes from the declaration of CallsFunction; because the variable has been typed using the CallsFunction interface, the compiler infers the type of the cb parameter based on the interface.

Listing 2-5. Bottom-up and top-down inference

function add(a: number, b: number) {
    /* The return value is used to determine
       the return type of the function */
    return a + b;
}

interface CallsFunction {
    (cb: (result: string) => any): void;
}

// The cb parameter is inferred to be a function accepting a string
var callsFunction: CallsFunction = function (cb) {
    cb('Done'),

    // Supplied parameter does not match any signature of the call target
    // cb(1);
};

// The result parameter is inferred to be a string
callsFunction(function (result) {
    return result;
});

Best Common Type

The type of the example variable in Listing 2-6 looks a lot like the single object passed into the array literal. If you were writing the type annotation for this type it would be { a: string, b: number, c: boolean }[] and this is the type you will see if you hover over the example array in your development tools. This is the premise for understanding best common types.

Listing 2-6. Best common type premise

var bestCommonTypeExample = [
    { a: 'A', b: 1, c: true }
];

for (var i = 0; i < bestCommonTypeExample.length; i++) {
    var value = bestCommonTypeExample[i];

    console.log(value.a);

    console.log(value.b);

    console.log(value.c);
}

If you were to add a second item to the array literal with different properties, there are several possible outcomes.

  • If the item is an object with an identical signature, the type of the example variable will be unaffected.
  • If the item is an object with no matching properties, the type of the example variable will be updated to {}[]—simply an array of objects.
  • If the item is an object with some matching properties, the type of the example variable will be updated to the best common type.

The best common type includes all properties with the same names and types that are present in all of the values in the expression. Any properties that fail to match throughout the expression are dropped from the best common type. In Listing 2-7, the type used for the example variable has been reduced to the only common property in all values, { b: number }[].

Listing 2-7. Best common type—example

var example = [
    { a: 'A', b: 1, c: true },
    { a: 'B', b: 2 },
    { b: 3 }
];

for (var i = 0; i < bestCommonTypeExample.length; i++) {
    var value = bestCommonTypeExample[i];

    // The property 'a' does not exist on value of type '{ b: number }'.
    // console.log(value.a);

    // OK
    console.log(value.b);

    // The property 'c' does not exist on value of type '{ b: number }'.
    // console.log(value.c);
}

The process of determining the best common type is not just used for array literal expressions; they also are used to determine the return type of a function or method that contains multiple return statements.

Contextual Types

Contextual types are a good example of how advanced type inference can be. Contextual typing occurs when the compiler bases its types on the location of an expression. In Listing 2-8, the type of the event parameter is determined by the known signature of the window.onclick definition. The inference is not just limited to the parameters, the entire signature, including the return value, can be inferred because of the existing knowledge of the window.onclick signature.

Listing 2-8. Contextual types

window.onclick = function(event) {
        var button = event.button;
};

Widened Types

The term widened type refers to situations in TypeScript where the type of a function call, expression, or assignment is null or undefined. In these cases, the type inferred by the compiler will be the widened any type. In Listing 2-9, the widened variable will have a type of any.

Listing 2-9. Widened types

function example() {
        return null;
}

var widened = example();

When to Annotate

Because type inference has been a key feature of TypeScript since day one, the discussion on when to make types explicit with type annotations can take place without controversy. This has been a tricky topic for statically typed languages that have later decided to add some level of support for type inference.

The final decision about the level of type annotations you add to your program should be made jointly between all team members, but you may wish to use the following suggestions as a starting point for your discussion.

Use as few type annotations as possible. If the type can be inferred, allow it to be inferred. You may want to make return types explicit (especially for void return types) and in most cases you should annotate parameters as part of a method signature, but outside of these cases you should put your faith in the compiler as far as you can. You can get the compiler to warn you about cases where it can’t find a type using a special flag (--noImplicitAny) that prevents the any type from being inferred. You can read more about this flag in Appendix 2.

Duplicate Identifiers

On the whole, you should do your best to avoid name clashes in your program. TypeScript supplies the tools to make name clashes unnecessary by allowing you to move your program out of the global scope. However, there are some interesting features around identifiers in TypeScript including many situations where you are allowed to use the same name within the same scope.

In most cases, the use of an existing class or variable name within the same scope will result in a “Duplicate identifier” error. No particular structure gets preferential treatment; the later of the two identifiers will be the source of the error. If you create a module with a duplicate identifier, the error won’t show until you implement the body of the module. This is because TypeScript is clever enough to delete empty modules from the compiled code (which in turn fixes the duplicate identifier error).

One valid use of a duplicate identifier is with interfaces. Once again, the compiler knows that there will be no duplicate identifier at runtime because interfaces are erased during compilation; its identifier will never appear in the JavaScript output. The use of a duplicate identifier for an interface and a variable is a common pattern in the TypeScript library, where the standard types are defined using an interface and then allocated to a variable declaration with a type annotation. Listing 2-10 shows the TypeScript library definition for DeviceMotionEvent. The interface for DeviceMotionEvent is immediately followed by a variable declaration with the same DeviceMotionEvent identifier.

Listing 2-10. TypeScript DeviceMotionEvent

interface DeviceMotionEvent extends Event {
    rotationRate: DeviceRotationRate;
    acceleration: DeviceAcceleration;
    interval: number;
    accelerationIncludingGravity: DeviceAcceleration;
    initDeviceMotionEvent(
                type: string,
                bubbles: boolean,
                cancelable: boolean,
                acceleration: DeviceAccelerationDict,                 accelerationIncludingGravity: DeviceAccelerationDict,
                rotationRate: DeviceRotationRateDict,
                interval: number): void;
}

declare var DeviceMotionEvent: {
    prototype: DeviceMotionEvent;
    new (): DeviceMotionEvent;
}

Ambient Declarations are explained in more detail later in this chapter, but this technique works just as well without the declare keyword before the variable declaration. The use of interfaces in the standard library is a deliberate choice. Interfaces are open, so it is possible to extend the definitions in additional interface blocks. If a new web standard was published that added a motionDescription property to the DeviceMotionEvent object, you wouldn’t have to wait for it to be added to the TypeScript standard library; you could simply add the code from Listing 2-11 to your program to extend the interface definition.

Listing 2-11. Extending the DeviceMotionEvent

interface DeviceMotionEvent {
    motionDescription: string;
}

// The existing DeviceMotionEvent has all of its existing properties
// plus our additional motionDescription property
function handleMotionEvent(e: DeviceMotionEvent) {
    var acceleration = e.acceleration;
    var description = e.motionDescription;
}

All of the interface definition blocks from the same common root are combined into a single type, so the DeviceMotionEvent still has all of the original properties from the standard library and also has the motionDescription property from the additional interface block.

Type Checking

Once a schedule of types has been gathered from your program, the TypeScript compiler is able to use this schedule to perform type checking. At its simplest, the compiler is checking that when a function is called that accepts a parameter of type number; all calling code passes an argument with a type that is compatible with the number type.

Listing 2-12 shows a series of valid calls to a function with a parameter named input, with a type of number. Arguments are accepted if they have a type of number, enum, null, undefined, or any. Remember, the any type allows dynamic behavior in TypeScript, so it represents a promise from you to the compiler saying that the values will be acceptable at runtime.

Listing 2-12. Checking a parameter

function acceptNumber(input: number) {
    return input;
}

// number
acceptNumber(1);

// enum
acceptNumber(Size.XL);

// null
acceptNumber(null);

As types become more complex, type checking requires deeper inspection of the objects. When an object is checked, each member of the object is tested. Public properties must have identical names and types; public methods must have identical signatures. When checking the members of an object, if a property refers to a nested object, the inspection continues to work down into that object to check compatibility.

Listing 2-13 shows three differently named classes and a literal object that show all are compatible as far as the compiler is concerned.

Listing 2-13. Compatible types

class C1 {
    name: string;

    show(hint?: string) {
        return 1;
    }
}

class C2 {
    constructor(public name: string) {

    }

    show(hint: string = 'default') {
        return Math.floor(Math.random() * 10);
    }
}

class C3 {
    name: string;

    show() {
        return <any> 'Dynamic';
    }
}

var T4 = {
    name: '',
    show() {
        return 1;
    }
};

var c1 = new C1();
var c2 = new C2('A name'),
var c3 = new C3();

// c1, c2, c3 and T4 are equivalent
var arr: C1[] = [c1, c2, c3, T4];

for (var i = 0; i < arr.length; i++) {
        arr[i].show();
}

The notable parts of this example include the name property and the show method. The name property must exist on the object, it must be public, and it must be a string type. It doesn’t matter whether the property is a constructor property. The show method must return a type compatible with number. The parameters must also be compatible—in this case the optional hint parameter can be matched using a default parameter or by omitting the parameter entirely. If a class had a mandatory hint parameter, it would not be compatible with the types in Listing 2-13. As shown in the fourth type, literal objects can be compatible with classes as far as the compiler is concerned, as long as they pass the type comparison.

Ambient Declarations

Ambient declarations can be used to add type information to existing JavaScript. Commonly, this would mean adding type information for your own existing code, or for a third-party library that you want to consume in your TypeScript program.

Ambient declarations can be gradually constructed by starting with a simple imprecise declaration and turning up the dial on the details over time. Listing 2-14 shows an example of the least precise ambient declaration you can write for the jQuery framework. The declaration simply notifies the compiler that an external variable will exist at runtime without supplying further details of the structure of the external variable. This will suppress errors for the $ variable, but will not supply deep type checking or useful autocompletion.

Listing 2-14. Imprecise ambient declaration

declare var $: any;

$('#id').html('Hello World'),

All ambient declarations begin with the declare keyword. This tells the compiler that the following code block contains only type information and no implementation. Blocks of code created using the declare keyword will be erased during compilation and result in no JavaScript output. At runtime, you are responsible for ensuring the code exists and that it matches your declaration.

To get the full benefit of compile-time checking, you can create a more detailed ambient declaration that covers more of the features of the external JavaScript that you use. If you are building an ambient declaration, you can choose to cover the features you use the most, or the higher-risk features that you judge to be the most likely source of type errors. This allows you to invest in defining the type information that gives you the most return on your time investment.

In Listing 2-15 the jQuery definition has been extended to cover the two elements used in the first example; the selection of an element using a string query containing the element’s id and the setting of the inner HTML using the html method. In this example, a class is declared called jQuery, this class has the html method that accepts a string. The $ function accepts a string query and returns an instance of the jQuery class.

Listing 2-15. Ambient class and function

declare class jQuery {
    html(html: string): void;
}

declare function $(query: string): jQuery;

$('#id').html('Hello World'),

When this updated ambient declaration is used, autocompletion supplies type hints as shown in Figure 2-3. Any attempt to use a variable, function, method, or property that isn’t declared will result in a compiler error and all arguments and assignments will also be checked.

9781430267911_Fig02-03.jpg

Figure 2-3. Ambient declaration autocompletion

It is possible to create ambient declarations for variables, functions, classes, enumerations, and both internal and external modules. Interfaces appear to be missing from this list, but interfaces are already analogous to ambient declarations as they describe a type without resulting in any compiled code. This means you can write ambient declarations using interfaces, but you would not use the declare keyword for interfaces.

Image Note  In reality, it actually makes more sense to declare jQuery as an interface rather than a class because you cannot instantiate instances of jQuery using var jq = new jQuery();. All you would need to do is change the class keyword to the interface keyword because neither needs implementation when used in a declaration.

Declaration Files

Although it is possible to place ambient declarations in any of your TypeScript files, there is a special naming convention for files that contain only ambient declarations. The convention is to use a .d.ts file extension. Each module, variable, function, class, and enum in the file must be preceded by the declare keyword, and this is enforced by the TypeScript compiler.

To use a declaration file from within your program, you can refer to the file just like any other TypeScript file. You can use reference comments, or make the file the target of an import statement. When using import statements to target a file, the declaration file should be placed in the same folder and have the same name as the JavaScript file as shown in Figure 2-4.

9781430267911_Fig02-04.jpg

Figure 2-4. Declaration files

Definitely Typed

If you plan to write an ambient declaration for any of the common JavaScript libraries or frameworks, you should first check to see if someone has already done the hard work by visiting the online library for ambient declarations, Definitely Typed:

https://github.com/DefinitelyTyped

The Definitely Typed project, started by Boris Yankov, contains over 180 definitions for popular JavaScript projects including Angular, Backbone, Bootstrap, Breeze, D3, Ember, jQuery, Knockout, NodeJS, Underscore, and many others. There are even declarations for unit testing frameworks such as Jasmine, Mocha, and qUnit. Some of these external sources are incredibly complex, so using an existing declaration can save a great deal of time.

Summary

Working within the TypeScript type system requires at least a passing knowledge of the difference between nominal and structural typing. Structural typing can make some designs a little tricky, but doesn’t prevent you from using any patterns that you may wish to transfer from a nominally typed system. Structural typing allows you to leave out type annotations in favor of allowing the types to be inferred throughout your program.

When you compile your program, the types are checked against the explicit and implicit types, allowing a large class of errors to be detected early. You can opt out of type checking for specific parts of your program using the any type.

You can add type information for JavaScript code by creating or obtaining ambient declarations for the JavaScript code. Usually these ambient declarations would be stored in a declaration file that sits alongside the JavaScript file.

Key Points

  • Static type checking is optional.
  • TypeScript is structurally typed.
  • All type information is removed during compilation.
  • You can let the compiler work out the types for you using type inference.
  • Ambient declarations add type information to existing JavaScript code.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.227.46.69