CHAPTER 4
A statically typed language gives you compile-time checking for type safety. This doesn’t mean you have to specify all types explicitly, as a smart compiler can infer the types in many cases. TypeScript is no exception and the compiler will intelligently determine types for you even if you don’t explicitly declare the types in your code.
TypeScript is referred to as optionally statically typed, which means you can ask the compiler to ignore the type of a variable if you want to take advantage of dynamic typing in a particular circumstance. This mix of static and dynamic typing is already present in .NET; for example, C# is statically typed but allows dynamic types to be declared with the dynamic keyword.
As well as compile-type checking, the language constructs allow static analysis that makes it possible for development tools to highlight errors in your code at design time without the need for compilation.
Throughout this chapter, I will give practical examples that will show you how to take advantage of TypeScript’s static analysis and compilation.
Before I start introducing the TypeScript language features, here is a plain and simple JavaScript logging function. This is a common paradigm in JavaScript development that checks for the presence of a console before attempting to write a message to it. If you don’t perform this check, calling the console results in an error when one isn’t attached.
Logging Function
function log(message) { if (typeof window.console !== 'undefined') { window.console.log(message); } } var testLog = "Hello world"; log(testLog); |
Even though this example is plain JavaScript, the TypeScript compiler is smart enough to infer that testLog is a string. If you hover over the variables and functions in your development environment you’ll see the following tooltips.
Types in Sample2.ts
Name | Tooltip |
|---|---|
log | (message: any) => void |
message | any |
window.console | Console |
testLog | string |
There are several benefits to this type knowledge when it comes to writing your code. Firstly, if you try to assign a value to testLog that isn’t a string type, you will get a design-time warning and a compilation error.
Because TypeScript is aware of the types, development tools are able to supply more precise autocompletion than they can for JavaScript. For example, if you didn’t know the type of testLog, you would have to supply an autocompletion list that either covered all possible suggestions for all types or no suggestions at all. Because TypeScript knows the type is string, the autocompletion can contain just the properties and operations relevant to a string.

Precise autocompletion
This is a demonstration of type inference, where the tools work out the types based on the values you assign in code. It isn’t always possible or desirable to infer the types automatically like this, and that is the reason the message parameter has the dynamic any type, which means it is not statically typed. You could argue that based on the calling code, which always passes a string, it is possible to infer that message is also of type string. The problem with this kind of inference is that it relies on all calling code being available at compilation time, which is almost certainly not the case–so rather than make a potentially dangerous assumption, TypeScript uses the any type.
You don’t have to rely on type inference in your code, and you can use type declarations to tell the compiler the intended type of a variable, function, or parameter in cases where it cannot work it out. In the logging example, you only need to tell TypeScript the type of the message parameter in order for everything to be statically typed. To declare a type, append a colon followed by the type name; in this case, you would decorate the message parameter with a : string declaration
Typed Parameters
function log(message: string) { if (typeof window.console !== 'undefined') { window.console.log(message); } } var testLog = "Hello world"; log(testLog); |
Now when you view the tooltips for this code, you will see that everything is statically typed and the dynamic any keyword has been replaced in both the log function signature and the message parameter:
Types in Sample3.ts
Name | Tooltip |
|---|---|
log | (message: string) => void |
message | string |
window.console | Console |
testLog | string |
You can test the static typing by calling the log method with different values. Your development tools should highlight the erroneous calls that fail to pass a string argument. This will work in all cases where there is a type, whether it is inferred by the compiler or explicitly declared in your code.
Function Call Type Checking
// allowed log("Hello world"); // not allowed log(1); log(true); log({ 'key': 'value' }); |
The level of type declarations you add yourself is a matter of taste; you could explicitly declare the types everywhere in your code, but I personally find the resulting code too verbose, and in many cases adding a type declaration is redundant when the type is already obvious. The next two examples compare two approaches to help illustrate this subject. The first is an entirely explicit version of the logging code and the second applies a more pragmatic level of declarations.
Entirely Explicit Types
function log(message: string): void { if (typeof window.console !== 'undefined') { window.console.log(message); } } var testLog: string = "Hello world"; log(testLog); |
Pragmatic Explicit Types
function log(message: string): void { if (typeof window.console !== 'undefined') { window.console.log(message); } } var testLog = 'Hello world'; log(testLog); |
In the second example, I explicitly state the type for any variable or parameter that cannot be inferred by the compiler and additionally make the return type explicit for functions, which I find makes the function more readable. I haven’t specified the type for the testLog variable as it is obvious that it is a string because it is initialized with a string literal. I will go into more detail in the section When to Use Types at the end of this chapter.
TypeScript has five built-in primitive types as well as the dynamic any type and the void return type. The any type can have any value assigned to it, and its type can be changed at runtime, so it could be initialized with a string and later overwritten with a number, or even a custom type. The void return type can only be used to declare that a function does not return a value.
The following list contains the five built-in primitive types.
Primitive Types
Name | Tooltip |
|---|---|
number | The number type is equivalent to the primitive JavaScript number type, which represents double-precision 64-bit floating-point values. If you are used to the distinction of integer types and non-whole numbers, there is no such distinction in TypeScript. |
boolean | The boolean type represents a value that is either true or false. |
string | The string type represents sequences of UTF-16 characters. |
null | The null type always has the value of the null literal. This is a special sub-type that can be assigned to a variable of any type except for undefined or void. |
undefined | The undefined type always has the value of the undefined literal. This is also a special sub-type, but unlike null, it can be assigned to any type. |
Use of Null and Undefined
// allowed var a: string = null; var b: number = undefined; // not allowed var c: null; var d: undefined; // has a value of undefined var e: string; |
In this example I have demonstrated that you can assign null or undefined to a variable with an explicit type, but you cannot declare something to be of type null or undefined.
You are not restricted to the built-in types. You can create your own types using modules, interfaces, and classes, and use these in your type declarations. In this example I haven’t even created a definition for the interface, or a body for the class, but they can already be used as valid types within the type system. In addition, the type system works with polymorphism, which I discuss in more detail in Inheritance and Polymorphism.
Custom Types Using Interfaces and Classes
interface ILogger { } class Logger { } var loggerA: ILogger; var loggerB: Logger; |
It is technically possible to use a module in a type declaration, but the value of this is limited as the only thing you could assign to the variable is the module itself. It is rare to pass a whole module around your program and much more likely that you will be passing specific classes, but it is worth knowing that it can be done.
Using a Module as a Type
module MyModule { } var example: MyModule = MyModule; |
The type definitions I have described so far are unlikely to be enough when it comes to writing a program. TypeScript has advanced type declarations that let you create more meaningful types, like statically typed arrays or callback signatures. There is a distinct cognitive shift from .NET type declarations for these, which I will explain as we look at each case.
The first advanced type declaration allows you to mark the type of an array by adding square brackets to the type declaration. For example, an array of strings would have the following type declaration:
var exampleA: string[] = []; |
Unlike other languages you may be using, the variable is initialized using either the array literal of empty square brackets [] or the new Array(10) constructor if you wish to specify the array length. The type is not used on the right-hand side of the statement.
The second advanced type declaration allows you to specify that the type is a function. You do this by surrounding the definition in curly braces; for example, a function accepting a string parameter and not returning any value would have the following type declaration:
var exampleA: { (name: string): void; } = function (name: string) { }; |
If the function has no parameters, you can leave the parenthesis empty, and similarly you can specify a return value in place of the void return type in this example.
In all of these cases, the compiler will check assignments to ensure they comply with the type declaration, and autocompletion will suggest relevant operations and properties based on the type.
Here are some examples of advanced type declarations in action:
Advanced Type Declarations
class Logger { } // exampleA's type is an array of Logger objects. var exampleA: Logger[] = []; exampleA.push(new Logger()); exampleA.push(new Logger()); // exampleB's type is a function. // It accepts an argument of type string and returns a number. var exampleB: { (input: string): number; }; exampleB = function (input: string) { return 1; }; // exampleC's type is an array of functions. // Each function accepts a string and returns a number. var exampleC: { (input: string): number; } [] = []; function exampleCFunction(input: string) : number { return 10; } exampleC[0] = exampleCFunction; exampleC[1] = exampleCFunction; |
I explained a little about type inference at the start of this chapter, but the purpose of this section is to clarify exactly how it works in TypeScript. Inference only takes place if you haven’t explicitly stated the type of a variable, parameter, or function.
For variables and parameters that are initialized with a value, TypeScript will infer the type from the initial value. For example, TypeScript infers the correct types for all of the following examples by inspecting the initial value even if the initial value comes from another variable:
Type Inference
class ExampleClass { } function myFunction(parameter = 5) { // number } var myString = 'Hello World'; // string var myBool = true; // boolean var myNumber = 1.23; // number var myExampleClass = new ExampleClass(); // ExampleClass var anotherBool = myBool; // boolean |
If you don’t initialize the variable or parameter with a value, it will be given the dynamic any type, even if you assign a value of a specific type to it later in the code.
Inferred Any Type
var example; // any example = 'string'; // still any |
Type inference for functions usually works upwards from the bottom, determining the return value based on what is being returned. When writing a function, you will be warned if you have conflicting return types—for example, if one branch of code returns a string and another branch of code returns a number.
Incompatible Return Types
function example (myBool: boolean) { if (myBool) { return 'hello'; // string } return 1; // number } |
In some situations, TypeScript uses the contexts within which an expression occurs to determine the types. For example, if a function expression is being assigned to a variable whose type is known, the type declaration of the variable will be used to infer the types for parameters and return values.
Contextual Typing
var func: { (param: number): number; }; func = function (a) { return a++; } |
This example demonstrates that the type of a is inferred to be number, based on the type declaration of the func variable on the first line.
Type inference uses a widened type when the initial value of a variable, parameter, or function return value is deemed to be null or undefined. In these cases, the dynamic any type is used.
Widened Types
var a = undefined; // any var b = null; // any var c = [null, null]; // any[] var d = { a: undefined, b: 1 }; // { a: any, b: number } |
Whether to make a type explicit or not is a matter of personal taste, so you should agree on your team’s preferred style in this respect. The discussion is similar to the conversations I have heard over the use of the .NET var keyword. The important thing to remember is that whether a type is explicit or inferred, it is still statically typed in all cases where the type has not been widened.
The minimum level of explicit typing would be to specify types wherever the compiler would infer the any type either because a variable hasn’t been initialized, or it has been initialized with a null or undefined value. I would recommend explicitly declaring types on interfaces, and on function parameters and return values, in addition to this minimum level.