TypeScript Tutorial

TypeScript Tutorial

JavaScript is considered a programming lingua franca of the modern world. Primarily, it was dedicated to elementary browser document operations. Now, it underpins complex client- and server-side applications. Maybe due to its intricate evolution or maybe due to architectural decisions JavaScript does not feature a strong typing solution for its values. TypeScript began its life as a Microsoft endeavor to address that issue but it became a powerful system of static (i.e. pre-runtime) checking of JavaScript code from typing and other perspectives and even enhancing it with some runtime elements such as enums.

What is TypeScript?

Introduction

TypeScript is a programming language developed by Microsoft to introduce stronger object typing to JavaScript. One of the main architects of TypeScript was Anders Hejlsberg. The first public version of TypeScript (0.8) appeared in 2012.

In time, TypeScript became much more than a strong object typing solution for JavaScript. TypeScript version 4 enhances JavaScript development with:

  • static type checking of variable values,

  • static type checking of function arguments,

  • static checking of certain operations,

  • static control flow analysis, and

  • some non-static (i.e. available at runtime) extensions to JavaScript such as enums.

There are the two crucial concepts to grasp when it comes to type checking in TypeScript:

  • the TypeScript type checking is static,

  • the TypeScript type checking of object values is structural.

It is also worth noting that from a syntax perspective TypeScript is a strict superset of JavaScript which means that any JavaScript code is a valid TypeScript code from a syntactic perspective. However, not all TypeScript code is valid JavaScript code.

Static Type Checking of Variable Values

In JavaScript, a variable declared with let (or var) can be reassigned not only to a different value of the same type but also to a different value of a different type. Further, it is not possible in JavaScript to restrict the possibility of re-assignment of a variable declared with let (or var) to a value of a different type.

// Pure JavaScript
let firstName = 'Joe'
firstName = 42 // Allowed re-assignment.

TypeScript can be used to restrict such a possibility.

// TypeScript
let firstName: string = 'Joe'
firstName = 42 // TS Problem: Type 'number' is not assignable to type 'string'.

In the above example TypeScript rises a problem of assignment of a value of type number to the variable with a restricted value type of string.

The type checking of assignments and re-assignment of variables by TypeScript takes place before JavaScript runtime.

Static Type Checking of Functions #1

In JavaScript, it is not possible to restrict value types of arguments that are being passed to functions.

// JavaScript
function multiply(a, b) {
  return a * b
}

multiply(2,3) // => 6
multiply('two', { num: 3 }) // => NaN

TypeScript can be used to restrict such a possibility.

// TypeScript
function multiply(a: string, b: string) {
  return a * b
}

multiply(2,3) // => 6
multiply('two', { val: 3 }) // TS Problem: Argument of type 'string' is not assignable to parameter of type 'number'
multiply(2, { val: 3 }) // TS Problem: Argument of type '{ val: number; }' is not assignable to parameter of type 'number'.

The type checking by TypeScript of arguments being passed to functions is static i.e. takes place before JavaScript runtime.

Static Checking of Certain Operations

In JavaScript calling undefined, null, a string, a number or other uncallable values during runtime results in TypeError: null is not a function. Such an error - if not caught - might crash the program.

const fooBar = {
  foo: () => "I'm callable!",
  bar: "I'm not!"
}

fooBar.foo() // => "I'm callable!"
fooBar.bar() // TypeError: fooBar.bar is not a function
fooBar.baz() // TypeError: fooBar.baz is not a function

TypeScript allows for catching such bugs before JavaScript runtime.

const fooBar = {
  foo: () => "I'm callable!",
  bar: "I'm not!"
}

fooBar.foo() // => "I'm callable!"
fooBar.bar() // TS Problem: This expression is not callable. Type 'String' has no call signatures.
fooBar.baz() // TS Problem: Property 'baz' does not exist on type '{ foo: () => string; bar: string; }'.

Static Control Flow Analysis

TypeScript can help detect some bugs through static control flow analysis.

if (true) {
  "Yes! It's true!"
} else {
  "Nay! Not true!" // TS Problem: Unreachable code detected.
}

Non-Static Extensions to JavaScript

The crucial functionality of TypeScript is static code checking from type and other perspectives. However, TypeScript introduces some runtime extensions to JavaScript. The most prominent of those are enums which shall be described in later chapters.

Static Code Checking

One of the most important concepts to grasp about TypeScript is that its code checking (including type checking) takes place before runtime.

To run a TypeScript file it first needs to be compiled to pure JavaScript. During compilation, all TypeScript code is removed with the exclusion of the TypeScript extensions to JavaScript such as enums.

// myApp.ts
let foo: string = 'foo'
let bar: number = 42

function calculateArea(length: number, width: number): number {
  return length * width
}

The above code cannot be executed by any JavaScript engine as JavaScript engines simply do not understand TypeScript. Let's try to execute the myApp.ts file with Node.js runtime environment that runs on the V8 engine.

$ node myApp.ts
# let foo: string = 'foo'
# SyntaxError: Unexpected token ':'

It throws Syntax Error. Therefore, the TypeScript code first needs to be compiled to pure JavaScript. To compile TypeScript code into JavaScript code use TypeScript Compiler called tsc that comes with the TypeScript installation.

$ tsc myApp.ts

After running the compiler the TypeScript code is compiled into the following pure JavaScript code:

// myApp.js
let foo = 'foo';
let bar = 42;

function calculateArea(length, width) {
  return length * width;
}

The code checking from type and other perspectives by TypeScript takes place during the above-described compilation from TypeScript code into JavaScript code.

In addition, many code editors such as Visual Studio Code, use TypeScript for code checking during code editing and are capable of presenting the programmer with errors visually even before compilation.

Built-In Types

Primitive Types

Primitive Types in JavaScript

In pure JavaScript, there are seven types of primitive values:

  • string,

  • symbol,

  • number,

  • bigint,

  • boolean,

  • undefined, and

  • null.

The above seven JavaScript primitive types characterize a given primitive value at runtime and can be checked dynamically at runtime using the keyword typeof.

typeof 'forty two' // => 'string'
typeof 42 // => 'number'
typeof BigInt(42) // => 'bigint'
typeof true // => 'boolean'
typeof undefined // => 'undefined'
typeof null // => 'object'

The expression typeof null returns 'object'. Although null is a primitive value it denotes an explicit indication of the absence of an object value and therefore typeof null returns 'object'.

Primitive Types in TypeScript

TypeScript have static counterparts of all JavaScript primitive types.

Therefore, the primitive types in TypeScript are:

  • string,

  • symbol,

  • number,

  • bigint,

  • boolean,

  • undefined, and

  • null.

TypeScript types characterize a given value statically i.e. before runtime.

Object Types

Objects in JavaScript

In pure JavaScript, any value that is not a primitive value must be an object.

A JavaScript object is a structural value that has properties which values reference other values of primitive and object types.

Functions are also objects in JavaScript.

An object in JavaScript can be created using:

  • literal notation - explicit syntactical depiction of a given object, or

  • the Object constructor.

Creating an Object in JavaScript

The primary way of creating an object in JavaScript is using curly braces literal notation, i.e. { and }, along with specifying zero or more properties.

const foo = {}
typeof foo // => 'object'

const bar = {
  baz: 42
}
typeof bar // => 'object'

However, it is also possible to create an object using the Object constructor.

const foo = new Object()
typeof foo // => 'object'

A way of creating an integer-keyed object, i.e. an array, is using square brackets literal notation, i.e. [ and ], along with specifying zero or more array values.

const foo = []
typeof foo // => 'object'

const bar = [42]
typeof bar // => 'object'

It is also possible to create an array using the Array constructor.

const foo = new Array(1,2,3)

Although values in arrays can be accessed using integers (0, 1, 2, etc.) in reality - under the hood - arrays are string indexed ('0', '1', '2', etc.). When accessing JavaScript array values using integers they are being cast to strings.

const foo = ['a', 'b', 'c']
foo[1] // => 'b'
foo['1'] // => 'b'
foo[1] === foo['1'] // => true

There are two ways of creating a callable object using the literal notation in JavaScript:

  • the function keyword, or

  • the arrow function syntax.

function foo() {}
typeof foo // => 'function'

const baz = () => {}
typeof baz // => 'function'

It is also possible to create a function using the Function constructor.

const add = new Function(['a', 'b'], 'return a + b')

Dynamic Object Type Checking in JavaScript

It is possible to check in JavaScript dynamically at runtime whether a given value is:

  • an object - using the typeof operator,

  • a specific integer-keyed object, i.e. an array - using Array.isArray method, or

  • a specific callable object, i.e. a function - again using typeof.

const foo = {}
typeof foo === 'object' // => true

const bar = []
typeof bar === 'object' // => true
Array.isArray(bar) // => true

const baz = () => {}
typeof baz === 'function' // => true

Further, it is possible to dynamically check in JavaScript at runtime whether a given object is an instance of another object using the instanceof operator. This allows checking whether a given object is for example an array or a function.

const foo = []
foo instanceof Array // => true
foo instanceof Object // => true
foo instanceof Function // => false

const baz = () => {}
baz instanceof Array // => false
baz instanceof Object // => true
baz instanceof Function // => true

Objects in TypeScript

During static (before runtime) code checking TypeScript allows - among other things - to restrict a given value to be:

  • an object,

  • a specific integer-keyed object, i.e. an array, or

  • a specific callable object, i.e. a function.

To restrict a given value to be an object and therefore disallow any non-object values the TypeScript type 'object' is used.

let foo: object
foo = {} // OK
foo = 42 // TS: Type 'number' is not assignable to type 'object'.

To restrict a given value to be an array and therefore disallow any non-array values the TypeScript type Array is used.

let foo: Array<any>
foo = [] // OK
foo = {} // TS: Type '{}' is missing the following properties from type 'any[]': length, pop, push, concat, and 29 more.

The Array type must specify a type of its allowable values.

For example, Array<any> allows values of any type. Array<string> allows values of string type only. Array<string | number> allows values of string or number types only.

Instead of Array<any> notation the alternative notation using square brackets can be used.

let foo: any[]
let bar: string[]
let baz: (string | number)[]

To restrict a given value to be a function and therefore disallow any non-function values the TypeScript specific notation is used.

let foo: () => void
foo = () => { console.log('Bar') } // OK
foo = {} // TS: Type '{}' is not assignable to type '() => void'. Type '{}' provides no match for the signature '(): void'.

Any

The TypeScript any type denotes a value that is allowed to be of any other TypeScript type.

A given value can be declared to be of any type:

  • explicitly, or

  • implicitly.

// Explicit declaration of bar as any.
function foo(bar: any) {
  console.log(bar)
}

// Implicit declaration of qux as any.
function baz(qux) {
  console.log(qux)
}

Whereas TypeScript always allows declaring a given value to be of any type explicitly the implicit any declaration might be disallowed using the noImplicitAny flag in tsconfig.json.

TypeScript allows calling any properties of values of any type. Compare it to the unknown type.

let foo: any
foo.bar() // TypeScript allows this but JavaScript will throw an error at runtime.

Although TypeScript will allow the above operation it will still fail during JavaScript runtime and the Uncaught TypeError: Cannot read properties of undefined (reading 'bar') error shall be thrown. This shows how overusing any might hinder the TypeScript type checking effort.

Unknown

The TypeScript unknown type denotes a value which type is not known at the time of static code checking as the value might be created dynamically during JavaScript runtime.

The difference between any and unknown is that at the time of the static code checking a value denoted with the any type is explicitly or implicitly allowed to be of any type whereas the type of value denoted with the unknown type is simply unknown which does not mean that it is allowed to be of the any type.

TypeScript does not allow calling properties of values of the unknown type. Compare it to the any type.

let foo: unknown
foo.bar() // TS: Property 'bar' does not exist on type 'unknown'.

Never

The TypeScript never type denotes a type representing a situation in which a value cannot be evaluated.

The never type is used:

  • to denote the type being returned by a function that never returns any value,

  • to denote the type of an already fully narrowed variable (from type perspective) in a specific block of code.

function foo(): never {
  throw new Error()
}

function bar(): never {
  while true()
}

let baz
if (baz === 'object') {
  console.log(baz)
} else if (baz !== 'object') {
  console.log(baz)
} else {
  // Unreachable block.
  console.log(baz) // The inferred type of baz in the else block is never.
}

Void

The TypeScript void type is used to denote that a function should not return any explicit value.

The function return type of void restricts the return value of a function based on the manner of the void type annotation. When the void return type is annotated directly in a function body the function should not return any value but an explicit return of undefined or null (provided strictNullChecks is false) is allowed by TypeScript.

function foo(): void {} // OK

function bar(): void {
  return
} // OK

function baz(): void {
  return undefined // OK
}

function qux(): void {
  return null // OK provided strictNullChecks is false.
}

function quux(): void {
  return 42
} // TS: Type 'number' is not assignable to type 'void'.

However, when the void return type is a part of a function type annotation TypeScript will allow any value to be returned by the function with that type.

const foo: () => void = () => 42 // OK

type Baz = () => void
const qux: Baz = function() {
  return 42
} // OK

Further reading on the subject: Why does TypeScript have both void and undefined?

Static Type Checking of Variables

Unrestricted Assignment to `var` & `let` Variables in JavaScript

In JavaScript, a value reference of a variable declared with var can be changed at runtime not only to another reference to a value of the same type but to another reference to a value of any type whatsoever.

// Pure JavaScript
var foo = 'forty two'
foo = 'forty three' // Acceptable assignment.
foo = 42 // Also acceptable assignment.

Similarly, a value reference of a variable declared with ES6 let can be changed at runtime not only to another reference to a value of the same type but to another reference to a value of any type whatsoever.

// ES6
let foo = 'forty two'
foo = 'forty three' // Acceptable assignment.
foo = 42 // Also acceptable assignment.

On the other hand, a reference to a value of a variable declared with ES6 const cannot be changed at all.

// ES6
const foo = 'forty two'
foo = 'forty three' // Uncaught TypeError: Assignment to constant variable.

This does not mean that the value referenced by a variable declared with ES6 const cannot change. Only the reference to that value is constant.

// ES6
const foo = []
foo.push(42)
console.log(foo) // [42]

Restricting Assignment to `var` & `let` Variables in TypeScript

Contrary to JavaScript, in TypeScript, it is possible to restrict the type of value allowed to be referenced by a given variable declared with var or let.

To restrict a type of a value allowed to be assigned to a variable follow the variable name with : and provide the type. This is called variable type annotation.

// TypeScript
let foo: string = 'forty two'
foo = 'forty three' // Acceptable assignment.
foo = 42 // TS: Type 'number' is not assignable to type 'string'.

let bar: number[] = [42]
bar = [43] // OK.
bar = ['forty two'] // Type 'string' is not assignable to type 'number'.

Variable Type Inference in TypeScript

For a variable declared with let or var, when no type is explicitly annotated, TypeScript implicitly infers the type of value allowed to be referenced by that variable.

// TypeScript
let foo = 'forty two' // TypeScript infers the type of foo to be type 'string'.
foo = 'forty three'  // Acceptable assignment.
foo = 42 // TS: Type 'number' is not assignable to type 'string'.

let bar = [42] // TypeScript infers the type of foo to be 'number[]'.
bar = [43] // OK.
bar = ['forty two'] // TS: Type 'string' is not assignable to type 'number'.

Static Type Checking of Functions

Unrestricted Function Argument Value Types in JavaScript

In JavaScript calling a function with arguments not congruent with defined function parameters is not considered an error.

function add(a, b) {
  return a + b
}
add(1, 2) // => 3
add('1', '2') // => '12'
add() // => NaN

Passing arguments of non-number values or not passing them at all is not considered an error by JavaScript and hence no error is thrown. Still, the function does not work as intended when non-number arguments are passed.

Further, an error might still occur when a function body is not as lenient as in the case of the add function.

function getCapitalizedName(person) {
  return person.name.charAt(0).toUpperCase() + person.name.slice(1)
}

getCapitalizedName({ name: 'joe' }) // 'Joe'
getCapitalizedName({ firstName: 'joe' }) // Uncaught TypeError: Cannot read properties of undefined (reading 'charAt').
getCapitalizedName() // Uncaught TypeError: Cannot read properties of undefined (reading 'name').

Restricting Function Argument Value Types in TypeScript

In TypeScript, it is possible to define function parameters in a way restricting the types of values allowed to be passed as arguments at the function call.

function add(a: number, b: number) {
  return a + b
}

add(1, 2) // 3
add('1', '2') // TS: Argument of type 'string' is not assignable to parameter of type 'number'.
add() // TS: Expected 2 arguments, but got 0.

Such static code checking of function argument values by TypeScript allows catching many bugs even before the program's runtime.

Function Return Values in JavaScript

All functions - unless interrupted or running indefinitely - return a value in JavaScript.

A function in JavaScript called without the new keyword can return:

  • a specific value explicitly by using the keyword return appended with the value,

  • undefined implicitly by using the keyword return with no value appended, or

  • undefined implicitly when the keyword return is omitted altogether.

function foo() {
  return 42
}
foo() // => 42

function bar() {
  return
}
bar() // => undefined

function qux() {}
qux() // => undefined

Further, a function called with the new keyword implicitly returns an object constructed through calling that function.

function user(name) {
  this.name = name
}
new user('Geralt') // => user {name: 'Geralt'}

In JavaScript, it is not possible to restrict a function return value to be of a specific type.

Restring Function Return Value Types in TypeScript

TypeScript allows restricting the allowed type to be returned by a function.

To restrict a function return type follow the function parameters with : and the specified type.

function add(a: number, b: number): number {
  return `Adding ${a} and ${b} gives ${a+b}.` // TS: Type 'string' is not assignable to type 'number'.
}

function add(a: number, b: number): number {
  return a + b // OK.
}

const getCapitalizedName = (person: { name: string }): string => {
  return person // TS: Type '{ name: string; }' is not assignable to type 'string'.
}

const getCapitalizedName = (person: { name: string }): string => {
  return person.name.charAt(0).toUpperCase() + person.name.slice(1) // OK.
}

Static Type Checking of Classes

Classes were introduced to JavaScript ecosystem by ES5.

Instance Members

Instance Properties

A class in JavaScript can have instance properties. They can be declared with or without initializers.

After the class initialization, the instance properties become properties of the object created through initialization.

class Foo {
  bar = 42
  baz
}

const qux = new Foo()
qux.bar // => 42
qux.baz // => undefined

TypeScript allows for restricting types of instance properties.

class Foo {
  bar: number = 42
  baz: string
}

const qux = new Foo()
qux.bar = 23 // OK
qux.bar = '23' // Type 'string' is not assignable to type 'number'.
qux.baz = '42' // OK
qux.baz = 42 // Type 'number' is not assignable to type 'string'.

If no type is annotated and no initializer is present at an instance property it has the implicit any type. If an initializer is present TypeScript will attempt type inference.

It is possible to enforce property initialization in constructor through setting the flag strictPropertyInitialization in tsconfig.json to true.

Instance Method Signatures

When an instance property's value is a function then the property is called an instance method.

Just like with non-method functions TypeScript allows for annotating instance method parameter types and return types.

class Student {
  firstName = 'Joe'

  changeFirstName(newFirstName: string): string {
    this.firstName = newFirstName

    return firstName
  }
}

const student = new Student()
student.changeFirstName('Other Joe') // => Student {firstName: 'Other Joe'}
student.changeFirstName(42) // TS: Argument of type 'number' is not assignable to parameter of type 'string'.

This in Instance Methods

Within a body of an instance method, the this keyword might but does not have to reference an instance of the class within the body of which the method was defined.

Generally, the value referenced by this in a JavaScript non-arrow function does not depend on how the function was defined but on how it is called. For extended reference see soundof.it JavaScript Tutorial - This.

class Captain {
  name = 'Hook'
  ship = 'Jolly Roger'

  logName() {
    console.log(this.name)
  }
}

const captain = new Captain()
const student = {
  name: 'Mark',
  logFavoriteCaptain: captain.logName
}

student.logFavoriteCaptain() // Mark

The probable intention of the above code was to log the student favorite captain's name. Instead, the name of the student was logged. TypeScript can catch such unintended operations through type annotation of this in instance methods.

class Captain {
  name = 'Hook'
  ship = 'Jolly Roger'

  logName(this: Captain) {
    console.log(this.name)
  }
}

const captain = new Captain()
const student = {
  name: 'Mark',
  logFavoriteCaptain: captain.logName
}

student.logFavoriteCaptain()
// The 'this' context of type '{ name: string; logFavoriteCaptain: (this: Captain) => void; }' is not assignable to method's 'this' of type 'Captain'.
// Type '{ name: string; logFavoriteCaptain: (this: Captain) => void; }' is missing the following properties from type 'Captain': ship, logName.

However, it needs to be remembered that TypeScript checks the compatibility of types structurally. Therefore, TypeScript will not find any problem with the following example.

class Captain {
  name = 'Hook'

  logName(this: Captain) {
    console.log(this.name)
  }
}

const captain = new Captain()
const student = {
  name: 'Mark',
  logName: captain.logName
}

student.logName() // Mark

The initial parameter named this in an instance method (or any other function) references the type of JavaScript this and is erased during compilation.

A thing to remember is that an arrow instance method behaves differently when it comes to this to the non-arrow instance method.

class Captain {
  name = 'Hook'

  logName = () => console.log(this.name)
}

const captain = new Captain()
const student = {
  name: 'Mark',
  logName: captain.logName
}

student.logName() // Hook

In other words, the value referenced by this within an arrow function does depend on how it was defined and not on how it is called.

Instance Property Accessibility

When it comes to accessibility an ES5 class can have two kinds of instance properties:

  • public - defined or declared without any prefix, and

  • private - defined or declared with # prefix.

At runtime, public properties are freely accessible. On the other hand, private properties are only accessible within the class in which they are declared.

class Foo {
  bar = 42
  #baz = 23
}

const qux = new Foo()
qux.bar // => 42
qux.baz // => undefined
qux.#baz // Uncaught SyntaxError: Private field '#baz' must be declared in an enclosing class

class Baz extends Foo {
  logBaz() {
    console.log(this.#baz) // Property '#baz' is not accessible outside class 'Foo' because it has a private identifier.
  }
}

Private properties defined or declared with # are ES5 - not TypeScript - specific feature.

TypeScript features its pre-compilation property modifiers when it comes to accessibility:

  • public

  • soft protected, and

  • soft private.

Public properties are denoted with no modifiers or by the keyword public and accessible without limitations.

Protected properties are denoted by the modifier protected and are accessible only within the class within which they were declared and within its subclasses.

Private properties are denoted by the modifier private and when using the dot notation are accessible only within the class within which they were declared.

The protected and private properties in TypeScript are called soft protected and soft private because it is still possible to access them using the bracket notation.

class Captain {
  public name = 'Hook'
  protected ship = 'Jolly Roger'
  private nemesis = 'Peter Pan'
}

const captain = new Captain()
console.log(captain.name) // 'Hook'
console.log(captain.ship) // Property 'ship' is protected and only accessible within class 'Captain' and its subclasses.
console.log(captain.nemesis) // Property 'nemesis' is private and only accessible within class 'Captain'.
console.log(captain['ship']) // 'Jolly Roger'
console.log(captain['nemesis']) // 'Peter Pan'

class starshipCaptain extends Captain {
  name = 'Jean Luc'
  ship = 'Enterprise'
  nemesis = 'Q'
}
// Class 'starshipCaptain' incorrectly extends base class 'Captain'.
// Property 'nemesis' is private in type 'Captain' but not in type 'starshipCaptain'.

In the above example the class starshipCaptain attempts to make the ship and nemesis properties public. It succeeds with regards to the ship property (as it is accessible to it) but fails with regards to the nemesis property (as it is inaccessible to it).

One other thing to remember about TypeScript private properties is that they are cross-instance accessible (similarly to Java, C# and PHP) as opposed to private properties in Ruby.

Constructors

Class constructors in JavaScript are functions that are being called at class instance initialization. During the call the this keyword in the constructor denotes the instance being created. It allows for initialization of not yet initialized properties and/or other operations.

A constructor always implicitly returns the instance and using the keyword return within its body is not allowed.

// ES5
class Student {
  firstName
  lastName

  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }
}

const student = new Student('Ciri', 'Riannon')
student // => Student {firstName: 'Ciri', lastName: 'Riannon'}
student.firstName // => 'Ciri'
student.lastName // => 'Riannon'

Just like with regular functions TypeScript allows for restricting types of constructor parameters.

// TypeScript
class Student {
  firstName: string
  lastName: string

  constructor(firstName: string, lastName: string) {
    this.firstName = firstName
    this.lastName = lastName
  }
}

const student = new Student(42, 42) // TS: Argument of type 'number' is not assignable to parameter of type 'string'.

Static Members

A class in JavaScript can have static members i.e. members that are not associated with and specific to instances of the class but with the class object itself.

class Captain {
  name: string
  static captainNames: string[] = []

  constructor(name: string) {
    this.name = name
    Captain.captainNames.push(name)
  }
}

const hook = new Captain('Hook')
const blackbeard = new Captain('Blackbeard')
const janeway = new Captain('Janeway')

console.log(Captain.captainNames) // (3) ['Hook', 'Blackbeard', 'Janeway']

Static class members are inherited.

Static class members can be made private at runtime using the # modifier. Further, TypeScript can alter their pre-runtime accessibility with public, protected and private modifiers.

Checking Implementation

TypeScript allows for static checking of congruency of class instance members minimal implementation with a specific contract outlined by an interface or a type alias.

To check whether a given class correctly implements an interface or a type alias the keyword implements is used appended with the interface name or type alias.

interface Captainable {
  name: string
  ship: string
}

class Captain implements Captainable {
  name: string
  ship: string
  nemesis: string // Additional property not present in the interface which is OK.
} // OK

class Student implements Captainable {
  firstName: string
  lastName: string
  major: string
}
// TS: Class 'Student' incorrectly implements interface 'Captainable'.
// TS: Type 'Student' is missing the following properties from type 'Captainable': name, ship.

In the above example the class Captain meets the interface Captainable contract as it has all the required properties with the specified types whereas the class Student does not.

A congruency with the implementation of more than one interface or type alias simultaneously is possible to be checked.

interface Nameable {
  name: string
}

interface Shipable {
  ship: string
}

class Captain implements Nameable, Shipable {
  name: string
  ship: string
} // OK

The implementation check with the implements keyword in no way supersedes or mimics the class extension using the keyword extends!

Static Checking of Certain Operations

Disallowing Calling Uncallable Values

Calling an uncallable value in JavaScript at runtime throws Uncaught TypeError.

// JavaScript
42() // Uncaught TypeError: 42 is not a function.

const witcher = { getName: () => 'Geralt' }
witcher.name() // Uncaught TypeError: witcher.name is not a function.

TypeScript allows preventing such errors pre-runtime through static code analysis.

const witcher = { name: 'Geralt' }
witcher.name() // TS: This expression is not callable. Type 'String' has no call signatures.

Disallowing Accessing Non-Existing Properties

Accessing a non-existing property of a non-null object in JavaScript does not throw an error and returns undefined.

// JavaScript
const witcher = { name: 'Geralt' }
witcher.firstName // undefined

TypeScript can prevent pre-runtime from trying to access a non-existing property of a non-null object.

// TypeScript
const witcher = { name: 'Geralt' }
witcher.firstName // TS: Property 'firstName' does not exist on type '{ name: string; }'.

Further, trying to access a property of a value that is undefined or null throws Uncaught TypeError: Cannot read properties of undefined / null.

// JavaScript
const empty = null
empty.some // Uncaught TypeError: Cannot read properties of null (reading 'some').

TypeScript can prevent pre-runtime from trying to access a property of undefined or null.

// TypeScript
const empty: null = null
empty.some // TS: Object is possibly 'null'.

Disallowing Nonsensical Arithmetic Operations

In JavaScript, some nonsensical arithmetic operations do not throw an error but still might have unintended consequences.

For example, dividing a number by a string in JavaScript does not throw an error but might return NaN.

// JavaScript
42 / 'forty two' // => NaN

TypeScript can prevent pre-runtime from such nonsensical arithmetic operations.

// TypeScript
42 / 'forty two' // => The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.

Static Control Flow Analysis

Unreachable Code

TypeScript is not all about static type checking. It also statically checks code from other perspectives.

For example, TypeScript informs about logical inconsistencies in control flow resulting in unreachable code.

if (true) {
  console.log('foo')
} else {
  console.log('bar') // TS: Unreachable code detected.
}

The flag allowUnreachableCode can be set to true in tsconfig.json to disallow unreachable code.

Always True or False Conditions

In some cases, TypeScript can indicate that a condition is always true or always false.

type Responses = '200' | '201'

const logResponse = (response: Responses) => {
  if (response !== '200' || response !== '201') { // TS: This condition will always return 'true' since the types '200' and '201' have no overlap.
    console.log(response)
  }
}

const logResponseButDifferent = (response: Responses) => {
  if (response === 200 && response === 201) { // TS: This condition will always return 'false' since the types '200' and '201' have no overlap.
    console.log(response)
  }
}

Non-Static Extensions to JavaScript

From Enums to Decorators

In addition to static pre-runtime code checking TypeScript also features some runtime extensions to JavaScript. The most prominent of those are enums. Another important example is decorators.

Enums

What Are Enums?

Enums are sets of constant values which:

  • semantically document a programmer's intent, and

  • possibly enhance the code with some additional type checking features.

Numeric Enums

The simplest form of an enum in TypeScript is a numeric enum.

A numeric enum can be defined providing constant members without providing any explicit initialized values which are then implicitly initialized as numbers and auto‑incremented from 0.

enum Groceries {
  Bread,
  Oranges,
  Beverages
}

At runtime, it is possible to access an enum value using a member.

Groceries.Bread // => 0

It is also possible to define a numeric enum with some or all non-explicitly initialized numeric values which are initialized implicitly using auto‑incrementation.

enum Groceries {
  Bread = 1,
  Oranges = 3,
  Beverages
}

console.log(Groceries) // => {1: 'Bread', 3: 'Oranges', 4: 'Beverages', Bread: 1, Oranges: 3, Beverages: 4}

Numeric enums - in addition to the so-called forward mapping from members to numeric values and as opposed to all other enums - feature the so-called reverse mapping from numeric values to members.

Therefore, at runtime, it is possible to access the member using a numeric value.

Groceries[0] // => 'Bread'

String Enums

String enums are enums which values are string literals or other enum members with string values.

enum Horses {
  Geralt = 'Roach',
  Ciri = 'Kelpie',
  Alexander = 'Bucephalus',
  WitcherHorse = Geralt
}

console.log(Horses) // => {Geralt: 'Roach', Ciri: 'Kelpie', Alexander: 'Bucephalus', WitcherHorse: 'Roach'}

String enums do not feature reverse mapping from values to members.

Heterogenous Enums

Theoretically it is possible to create enums with both numeric and string literal initializers.

Enums With Computed Values

Member values do not have to be initialized explicitly as numeric or string literals. They can be computed as long as the computed value is a number.

// OK.
const getAnswerToEverything = () => 42

enum Answers {
  theAnswerToEverything = getAnswerToEverything()
}

console.log(Answers) // => {42: 'theAnswerToEverything', theAnswerToEverything: 42}

// Not OK.
const getGeraltHorse = () => 'Roach'

enum Horses {
  geralt = getGeraltHorse() // TS: Only numeric enums can have computed members, but this expression has type 'string'. If you do not need exhaustiveness checks, consider using an object literal instead.
}

Members with computed values cannot come before members with implicitly initialized values.

enum Answers {
  theAnswerToEverything = getAnswerToEverything(),
  someOtherAnswer // TS: Enum member must have initializer.
}

An enum can have members with values computed at runtime.

enum Numbers {
  Random = Math.random()
}

A constant enum expression is an enum that can be fully evaluated at compile time, i.e. before runtime.

Enums As Types

A literal enum member is an enum member with:

  • an implicitly initialized numeric value, or

  • an explicitly initialized literal numeric value, or

  • a pre-runtime computed numeric value, or

  • a literal string value.

An enum which only has literal enum members:

  • becomes a union type of all its members, and

  • has its members become types themselves.

enum WitcherProvenances {
  Geralt = 'Rivia',
  Ciri = 'Cintra'
}

enum SorceressProvenances {
  Yennefer = 'Vengerberg',
  Triss = 'Maribor'
}

function getWitcherProvenance(witcher: WitcherProvenances) {
  return witcher
}

getWitcherProvenance(WitcherProvenances.Geralt) // => Rivia
getWitcherProvenance(SorceressProvenances.Yennefer) // TS: Argument of type 'SorceressProvenances.Yennefer' is not assignable to parameter of type 'WitcherProvenances'.

Other Enums

In addition to the enum types presented above TypeScript also features:

  • const enums which are unavailable at runtime, and

  • ambient enums which describe shapes of other enums.

The characteristic of the const enums and ambient enums fall outside the scope of this tutorial.

Decorators

Decorators are declarations that:

  • annotate a class (constructor), class method, class accessor, class property or a class parameter,

  • evaluate to functions which are then called at runtime and meta‑programmatically change the said elements.

function foo(constructor: Function) {
  console.log(constructor) // ƒ Bar() {}
  // do something with the constructor function
}

@foo
class Bar {
  constructor() {}
}

Decorators are currently only a proposal for JavaScript and an experimental feature in TypeScript.

To enable decorators in TypeScript use the experimentalDecorators flag in tsconfig.json.

The characteristic of decorators falls outside the scope of this tutorial.

Structural Types

Nominal v. Structural Type Checking

One of the most important concepts to grasp about TypeScript is that in TypeScript type checking is not nominal but structural.

In a nominal type system type compatibility of an object is being checked not based on its structure (its shape) but the basis of its class or prototype.

In a nominal type system an object value cannot be assigned to a variable, parameter or property when that object value is of a different class to the class declared for that variable, parameter or property irrespective of whether the object is compatible structurally.

class Student {
  first_name: string
  last_name: string
}

class Teacher {
  first_name: string
  last_name: string
}

student_a: Student = new Teacher() // An error is thrown!

C++, C#, Java, and Swift primarily use the nominal typing system.

On the other hand, in a structural type system an object value cannot be assigned to a variable, parameter, or property only when the object value differs structurally from the type declaration of that variable, parameter, or property, and classes are not taken into account.

TypeScript features structural type system and therefore the below is fully acceptable.

class Student {
  firstName: string
  lastName: string
}

class Teacher {
  firstName: string
  lastName: string
}

let studentA: Student = new Teacher() // OK!

In a structural type system, only an object's structure and not its class is taken into account when checking type compatibility.

TypeScript is an example of a structural type system. Other examples of structurally typed languages are Haskell and Elm.

Literal Structural Types

A literal structural type is a structural type without a name assigned to it.

A literal structural type cannot be declared or exist by itself. It can only be used as:

  • a variable type annotation,

  • an object property type annotation,

  • a function parameter type annotation,

  • a function return type annotation, and

  • a named structural type assignment.

// a variable type annotation
let car: { brand: string, year: number }

// an object property type annotation
let customer: { address: { street: string, city: string }}

// a function parameter type annotation
function logPerson(person: { firstName: string, lastName: string }) {
  console.log(person)
}

// a function return type annotation
function getAddress(person: { address: { street: string, city: string }}): { street: string, city: string } {
  return person.address
}

Named Structural Types

In TypeScript, a named structural type is declared using the keyword type appended with a name and with a literal structural type assigned to it.

type Student = {
  firstName: string,
  lastName: string
}

A property type of a structural type can be a named type itself.

type Address = {
  street: string,
  city: string
}

type Student = {
  firstName: string,
  lastName: string,
  address: Address,
  major: {
    name: string,
    department: string
  }
}

In the above example the address property type is specified using the named type Address and the major property type is specified using the literal type { name: string, department: string }.

Interfaces

Definition

As explained above, type aliases are reusable names (aka aliases) for existing primitive or object types or their unions.

On the other hand, interfaces are standalone blueprints or contracts describing a type structure (aka type shape) of an object value.

Interfaces v. Type Aliases

Both interfaces and type aliases pertain to a type of a given value. However, they differ in some respects. The following separates interfaces from type aliases:

  • definition & definition syntax,

  • type limitations,

  • declaration merging,

  • union limitations,

  • mapping limitations,

  • extension syntax,

  • error messages, and

  • performance.

Definition & Defintion Syntax

A type alias is a name for another type or union of types. A type alias does not define a new type by itself but only gives name to an existing one.

type Point2D = { x: number, y: number }
type Point = { x: number, y: number, z: number } | Point2D

On the other hand, an interface is not just a name for a type. An interface creates a new standalone blueprint or contract for a structural type. Not using an assignment operator at interface definition is symptomatic of that.

interface Point { x: number, y: number }

Type Limitations

Type aliases can be used for both primitive and object types.

type Word = string
type Person = { firstName: string, lastName: string }

On the other hand, interfaces cannot be used for primitive types. Even their syntax disallows that.

interface Person { firstName: string, lastName: string }

Declaration Merging

Probably the most important distinction between interfaces and type aliases is the ability of interfaces of declaration merging.

Using twice or more the same name for type aliases results in TypeScript problem Duplicate identifier.

type Point = { x: number, y: number }
type Point = { x: number, y: number, z: number } // TS: Duplicate identifier 'Point'..

However, it is acceptable to declare an interface with the same name more than once.

A redeclaration of an interface with the same name as the one already declared does not override it but merges with it creating a combination of properties of all declarations.

interface Point { x: number, y: number }
interface Point { z: number }

function logPoint(point: Point) {
  console.log(point)
}

logPoint({ x: 42, y: 42 })
/* TS: Argument of type '{ x: number; y: number; }' is not assignable to parameter of type 'Point'.
Property 'z' is missing in type '{ x: number; y: number; }' but required in type 'Point'. */

logPoint({ x: 42, y: 42, z: 42 }) // OK

The most prominent use case for declaration merging is a redeclaring type for an object imported from an external library which properties are being altered.

Union Limitations

Although, interfaces have the ability of redeclaration they are deemed fixed contracts or blueprints and therefore cannot define a union type like type aliases.

type Point2D = { x: string, y: string }
type Point3D = { x:string, y: string, z: string }
type Point = Point2D | Point3D // Not possible with interfaces.

Mapping Limitations

Using type aliases it is possible to map property keys from another type.

enum Coordinates { x, y, z }

type Point = {
  [key in Coordinates]: number
}

/* It defines the following type aliased with "Point":
type Point = {
  0: number;
  1: number;
  2: number;
} */

The above is not possible with interfaces.

enum Coordinates { x, y, z }

interface Point {
  [k in Coordinates]: number // TS: A mapped type may not declare properties or methods.
}

Extension Syntax & Limitations

Interfaces can be combined with other interfaces. Types can be combined with other types. Even interfaces and types can be combined.

An interface can be combined with other interface or type alias using the keyword extends. An extended interface has its properties combined with the other interface or type alias properties.

interface Point2D {
  x: number
  y: number
}

interface Point3D extends Point2D {
  z: number
}

const log3DPoint = (point: Point3D) => {
  console.log(point)
}

log3DPoint({ x: 42, y: 42 })
// TS: Argument of type '{ x: number; y: number; }' is not assignable to parameter of type 'Point3D'.
// TS: Property 'z' is missing in type '{ x: number; y: number; }' but required in type 'Point3D'.

An alias or literal type can be combined with an interface, an aliased type or a literal type using the & operator. When a type is being combined with an interface, aliased type or literal type it said that it is being intersected with it and the properties of those entities are being combined.

type Point2D = {
  x: number
  y: number
}

type Point3D = Point2D & {
  z: number
}

const log3DPoint = (point: Point3D) => {
  console.log(point)
}

log3DPoint({ x: 42, y: 42 })
// TS: Argument of type '{ x: number; y: number; }' is not assignable to parameter of type 'Point3D'.
// TS: Property 'z' is missing in type '{ x: number; y: number; }' but required in type '{ z: number; }'.

Error Messages & Editor Tooltips

As already noted defining an interface creates a new type blueprint or contract for an object whereas type alias only gives a reusable name to an already existing type.

Due to the above, interfaces and type aliases are presented differently in error messages and editor tooltips.

Performance

As presented under Microsoft Wiki TypeScript Performance extending interfaces as opposed to intersecting types is preferred due to among others performance issues.

Deriving Types From Types

One of the most powerful aspects of TypeScript is the system of creating types from other types (aka expressing types in terms of other types). The system includes:

  • generics,

  • produced literal key union types,

  • mapped types,

  • conditional types,

  • template literal types, and

  • index accessed types.

Generics

A generic is a type or an interface that is parametrized with one or more type parameters (aka type variables).

In TypeScript, it is possible to create a generic type alias or a generic interface but not a generic enum, nor generic namespace.

Generic Type Aliases

A generic type alias is a type alias that is parametrized with one or more type parameters.

type Items<Item> = {
  items: Item[]
}

type Car = {
  make: string,
  doors: number
}

type Cars = Items<Car>
// type Cars = { items: Car[] }

Generic Interfaces

A generic interface is an interface that is parametrized with one or more type parameters.

interface Items<Item> {
  items: Item[]
}

Generic Function Signatures

Generic function signatures are function signatures parametrized with one or more type parameters.

The below function allows a value to be pushed to an array only when the array already includes a value of the type as the value attempted to be pushed.

function pushToArray<T>(arr: T[], el: T): T {
  arr.push(el)

  return el
}

pushToArray([42], 23) // OK => 23
pushToArray([42], '23') // TS: Argument of type 'string' is not assignable to parameter of type 'number'.
pushToArray(['42'], '23') // OK => '23'
pushToArray(['42', 42], 23) // OK => 23
pushToArray(['42', 42], '23') // OK => '23'
pushToArray(['42', 42], {}) // TS: Argument of type '{}' is not assignable to parameter of type 'string | number'.

In the above example, the type of arr was inferred from the function call. But when an empty array would be pushed it would allow values of any type to be pushed. For cases such as this and for cases in which the compiler fails to infer types as intended it is possible to explicitly pass type to a generic function.

pushToArray<number>([], 23) // OK => 23
pushToArray<number>([], '23') // TS: Argument of type 'string' is not assignable to parameter of type 'number'.

The pushToArray function type can also be declared using arrow function type signatures, type aliases, and interfaces.

type PushToArray1 = <T>(arr: T[], el: T) => T

type PushToArray2 = {
  <T>(arr: T[], el: T): T
}

interface PushToArray3 {
  <T>(arr: T[], el: T): T
}

const pushToArray: PushToArray3 = (arr, el) => {
  arr.push(el)

  return el
}

Generic Classes

In addition to creating generic type aliases, generic interfaces, and generic function signatures, TypeScript allows for creating generic classes.

class Horse<Rider> {
  name: string
  rider: Rider
}

type Witcher = {
  name: string,
  school: string
}

type King = {
  name: string,
  kingdom: string
}

const geralt: Witcher = { name: 'Geralt', school: 'School of the Wolf' }
const alexander: King = { name: 'Alexander', kingdom: 'Macedon' }
const geraltHorse = new Horse<Witcher>()
geraltHorse.name = 'Roach'
geraltHorse.rider = geralt // OK.
geraltHorse.rider = alexander // TS: Property 'school' is missing in type 'King' but required in type 'Witcher'.

In the above example, a respective type is being passed to the Horse class at instance initialization syntactic indication (pre-runtime). It can also be done at class extension.

class WitcherHorse extends Horse<Witcher> {
  name: string
  rider: Witcher
}

One of the crucial things to remember about generic classes is that generic type parameters can only parametrize instance members and not static members.

Generic Constraints

It is possible to constraint the structural shape of types acceptable by function variable type parameters using the extends keyword.

type Swordsman = {
  name: string,
  swordsmanshipSkills: number
}

function logWitcher<Witcher extends Swordsman>(witcher: Witcher): void {
  console.log(witcher)
}

logWitcher({ name: 'Geralt', swordsmanshipSkills: 10 })
// {name: 'Geralt', hasSwordsmanshipSkills: true}

logWitcher({ name: 'Yennefer', magicSkills: 10 })
// TS: Argument of type '{ name: string; magicSkills: number; }' is not assignable to parameter of type 'Swordsman'.
// TS: Object literal may only specify known properties, and 'magicSkills' does not exist in type 'Swordsman'.

logWitcher({ name: 'Joe' })
// TS: Argument of type '{ name: string; }' is not assignable to parameter of type 'Swordsman'.
// TS: Property 'swordsmanshipSkills' is missing in type '{ name: string; }' but required in type 'Swordsman'.

Produced Literal Key Union Types

It is possible to create a new type being the literal union of keys of another type using the keyof operator.

type Captain = {
  name: string,
  ship: string,
  hasCaptainingSkills: true
}

type CaptainKeys = keyof Captain
// 'name' | 'ship' | 'hasCaptainingSkills'

const ship: CaptainKeys = 'ship' // OK
const anotherShip: CaptainKeys = 'plane' // Type '"plane"' is not assignable to type 'keyof Captain'.

Using the keyof operator on a type that has a string index signature produces the string | number union type.

type Foo = {
  [k: string]: any
}

type Bar = keyof Foo

let baz: Bar = 42 // OK
baz = '42' // OK
baz = {} // Type '{}' is not assignable to type 'string | number'.

Using the keyof operator on a type that has a number for an index signature produces the number type.

type Foo = {
  [k: number]: any
}

type Bar = keyof Foo

let baz: Bar = 42 // OK
baz = '42' // Type 'string' is not assignable to type 'number'.
baz = {} // Type '{}' is not assignable to type 'string | number'.

Mapped Types

What is a mapped type?

A mapped type is a structural type created through mapping over a union of keys of another structural type. Such a union is often created using the keyof operator. Mapped types are created using the index signature syntax.

Changing Property Types

A mapped type can be used to change a structural type property types.

type Numbered = {
  foo: number,
  bar: number,
  baz: number
}

type Stringified<T> = {
  [P in keyof T]: string
}

const qux: Numbered = { foo: 1, bar: 2, baz: 3 } // OK
const quux: Numbered = { foo: '1', bar: '2', baz: '3' } // TS: Type 'string' is not assignable to type 'number'.
const corge: Stringified<Numbered> = { foo: '1', bar: '2', baz: '3' } // OK

Changing Property Keys

A mapped type can be used to change (remap) keys of a structural type using the as operator combined with template literal types or other strategies.

type Numbered = {
  foo: number,
  bar: number,
  baz: number
}

type Underscored<T> = {
  [P in keyof T as `_${string & P}`]: T[P]
}

const qux: Underscored<Numbered> = { _foo: 1, _bar: 2, _baz: 3 } // OK
const quux: Underscored<Numbered> = { foo: 1, bar: 2, baz: 3 }
// TS: Type '{ foo: number; bar: number; baz: number; }' is not assignable to type 'Underscored<Numbered>'.
// TS: Object literal may only specify known properties, but 'foo' does not exist in type 'Underscored<Numbered>'. Did you mean to write '_foo'?

It is possible to change both property keys and property types simultaneously.

It is also possible to change the mutability and optionality modifiers through using the readonly and ? operators appended with - or the default +.

type Mutable<T> = {
  -readonly [P in keyof T]: T[P]
}

type Immutable<T> = {
  +readonly [P in keyof T]: T[P]
}

type Optional<T> = {
  [P in keyof T]+?: T[P]
}

type Required<T> = {
  [P in keyof T]-?: T[P]
}

Removing Selected Properties

A mapped type can be used to remove (filter out) selected properties through producing never type for a given property during mapping.

type Numbered = {
  foo: number,
  bar: number,
  baz: number
}

type Bazless<T> = {
  [P in keyof T as ('foo' | 'bar') & P]: T[P]
}

const qux: Bazless<Numbered> = { foo: 1, bar: 2 } // OK
const quxx: Bazless<Numbered> = { foo: 1, bar: 2, baz: 3 }
// Type '{ foo: number; bar: number; baz: number; }' is not assignable to type 'Bazless<Numbered>'.
// Object literal may only specify known properties, and 'baz' does not exist in type 'Bazless<Numbered>'.

Conditional Types

A conditional type is a type which depends on an assignability condition between two types. A conditional type condition mimics the JavaScript ternary operator and uses the extends keyword: type Foo = Bar extends Baz ? Qux : Quux.

type Captain = {
  name: string
}

type CaptainHook = {
  name: 'Hook',
  ship: 'Jolly Roger'
}

type Hookable = CaptainHook extends Captain ? CaptainHook : never
const hook: Hookable = { name: 'Hook', ship: 'Jolly Roger' } // OK
const witcher: Hookable = { name: 'Geralt' }
// Type '"Geralt"' is not assignable to type '"Hook"'.
// The expected type comes from property 'name' which is declared here on type 'CaptainHook'

In the above example, as CaptainHook is assignable to Captain the type Hookable is CaptainHook.

Conditional types can be used with generics.

type Captainable = {
  name: string,
  ship: string
}

type Shipable<T> = T extends { ship: string } ? T : never
type CaptainHook = Shipable<Captainable>
const hook: CaptainHook = { name: 'Hook', ship: 'Jolly Roger' }
const witcher: CaptainHook = { name: 'Geralt' } // TS: Property 'ship' is missing in type '{ name: string; }' but required in type 'Captainable'.

Template Literal Types

Template literal types are types utilizing string literal types and interpolation to create new string literal types.

type Hook = 'Hook'
type CaptainHook = `Captain ${Hook}`
const captainHook: CaptainHook = 'Captain Hook'
const captainBlackbeard: CaptainHook = 'Captain Blackbeard' // TS: Type '"Captain Blackbeard"' is not assignable to type '"Captain Hook"'.

The power of template literal types is their ability to create union types through a combination of interpolation and string literal types.

type Captains = 'Hook' | 'Blackbeard' | 'Picard'
type Captain = `Captain ${Captains}`
const hook: Captain = 'Captain Hook' // OK
const blackbeard: Captain = 'Captain Blackbeard' // OK
const tuvok: Captain = 'Captain Tuvok' // TS: Type '"Captain Tuvok"' is not assignable to type '"Captain Hook" | "Captain Blackbeard" | "Captain Picard"'. Did you mean '"Captain Hook"'?

Further, when more than one string literal type is interpolated in one template literal string they create a cartesian product of all possible combinations.

type Letters = 'A' | 'B' | 'C'
type Numbers = '1' | '2' | '3'
type LettersAndNumbers = `${Letters}${Numbers}`
const foo: LettersAndNumbers = 'A1' // OK
const bar: LettersAndNumbers = 'C2' // OK
const baz: LettersAndNumbers = 'D4' // TS: Type '"D4"' is not assignable to type '"A1" | "A2" | "A3" | "B1" | "B2" | "B3" | "C1" | "C2" | "C3"'.

As presented in earlier chapters template literal types can be used with generics.

Indexed Access Types

An indexed access type is a type created from a property of another type of structural nature.

The index access type syntax uses the square bracket notation to access the relevant property of another type.

type People = {
  captains: { name: string, ship: string },
  pilots: { name: string, plane: string }
}

type Captain = People['captains']
const hook: Captain = { name: 'Hook', ship: 'Jolly Roger' } // OK
const ameliaEarhart: Captain = { name: 'Amelia Earhart', plane: 'Lockheed Vega 5B' }

// TS: Type '{ name: string; plane: string; }' is not assignable to type '{ name: string; ship: string; }'.
// TS: Object literal may only specify known properties, and 'plane' does not exist in type '{ name: string; ship: string; }'.

Deriving Types From Values

When a value is syntactically assigned to a variable it also associates a type with that variable.

const geralt = {
  name: 'Geralt',
  swordsmanshipSkills: 10
}

type Witcher = typeof geralt
// type Witcher = {
//   name: string;
//   swordsmanshipSkills: number;
// }

One of the crucial points to observe is that the types of name and swordsmanshipSkills are not string literal type 'Geralt' and numeric literal 10 respectively but string and numeric. This is because TypeScript assumes that an object's properties can be changed. It is possible to tell TypeScript that the types of properties should be immutable literals using the as const operator.

const geralt = {
  name: 'Geralt',
  swordsmanshipSkills: 10
} as const

type Witcher = typeof geralt
// type Witcher = {
//   readonly name: "Geralt";
//   readonly swordsmanshipSkills: 10;
// }

As presented above a type implicitly assigned to a variable can be extracted using the typeof operator and used as any other type in TypeScript operations.

const geralt = {
  name: 'Geralt',
  swordsmanshipSkills: 10
}

type Witcher = typeof geralt

const eskel: Witcher = {
  name: 'Eskel',
  swordsmanshipSkills: 9
}

In addition to the possibility of extracting types from variables, it is also possible to extract types from properties of variables.

const geralt = {
  name: 'Geralt',
  swordsmanshipSkills: 10
}

type Name = typeof geralt.name
// type Name = string

Modules

ES Modules

TypeScript supports ES Modules system introduced to JavaScript by ES6 (ES2015).

In ES Modules any file with a top-level export, import and/or await statement is considered to be a module. A file without such a statement is considered to be a script and not a module.

Module statements and expressions are executed within their scope. Script statements and expressions are executed within the global scope.

To learn more about ES Modules see JavaScript Tutorial Modules.

TypeScript enhances ES modules with the possibility of exporting and importing types and enums.

While importing types optional prefix type can be used to make code transpiling easier.

// types.ts
type qux = object[]
export enum Foo { A, B, C }
export type Bar = number[]
export type Baz = string[]

export default qux
// file1.ts
import qux, { Foo, Bar, type Baz } from './types'
// file2.ts
import type qux from './types'

The only reservation is that a type-only import cannot specify a default import and named bindings at the same time.

// file3.ts
import type qux, { type Baz } from './types'
// TS: A type-only import can specify a default import or named bindings, but not both.

CommonJS Modules

TypeScript supports CommonJS modules.

CommonJS exports identifiers using the global object module and its method exports.

// file1.ts
const foo = 42
const bar = '42'

module.exports = {
  foo,
  bar,
  baz: 'forty two'
}

CommonJS imports identifiers using the global function require.

// file2.ts
const els = require('./file1')
console.log(els) // {foo: 42, bar: '42', baz: 'forty two'}

Simultaneous usage of ES Modules and CommonJS in TypeScript

In TypeScript simultaneous usage of ES Modules and CommonJS Modules is possible but fraught with hazards. TypeScript has a set of compiler flags to cope with that.

Module Resolution

Module resolution is the process of locating the imported (the required) module based on the provided location string.

TypeScript has two strategies of module resolution:

  • Classic, and

  • Node.

The description of the strategies falls outside the scope of this tutorial.

Namespaces

What are TypeScript Namespaces?

In TypeScript a namespace is an encapsulation of identifiers dedicated to the semantic separation of those identifiers from the global scope.

TypeScript namespaces are sometimes referred to as internal modules.

Namespaces (with the exclusion of ambient namespaces) are a TypeScript runtime extension to JavaScript and they compile to JavaScript objects enclosed within IIFEs (immediately invoked function expressions).

// TypeScript
namespace Foo {
  export const bar = 42
}
// Compiled JavaScript
"use strict";
var Foo;
(function (Foo) {
    Foo.bar = 42;
})(Foo || (Foo = {}));

Accessing Namespace Identifiers

Identifiers prefixed with the export keyword within a namespace can be accessed from outside the namespace but those not prefixed cannot.

The exported namespace identifiers can be accessed using the dot notation.

While accessing an exported namespace identifier it is allowed but not required to use the import keyword which is called aliasing.

// TypeScript
namespace Foo {
  export const bar = 42
  export const baz = '42'
  const qux = 'forty two'
}

const bar = Foo.bar
console.log(bar) // 42
import baz = Foo.baz
console.log(baz) // '42'
const qux = Foo.qux // TS: Property 'qux' does not exist on type 'typeof Foo'.
console.log(qux) // undefined
// Compiled JavaScript
"use strict";
var Foo;
(function (Foo) {
    Foo.bar = 42;
    Foo.baz = '42';
    const qux = 'forty two';
})(Foo || (Foo = {}));
const bar = Foo.bar;
console.log(bar);
var baz = Foo.baz;
console.log(baz);
const qux = Foo.qux;
console.log(qux);

Namespace Declaration Merging

Similarly to interfaces, multiple namespace declarations are merged.

// TypeScript
namespace Foo {
  export const baz = 42
}

namespace Foo {
  export const bar = '42'
}

console.log(Foo) // {baz: 42, bar: '42'}
// Compiled JavaScript
"use strict";
var Foo;
(function (Foo) {
    Foo.baz = 42;
})(Foo || (Foo = {}));
(function (Foo) {
    Foo.bar = '42';
})(Foo || (Foo = {}));
console.log(Foo);

Ambient Namespaces

To describe existing JavaScript modules or objects it is possible to use the so-called ambient namespaces i.e. namespaces encapsulating types only.

// TypeScript
declare namespace Foo {
  export interface Baz {
    bar: string
  }
}
// Compiled JavaScript
"use strict"

As presented above ambient namespaces are not compiled to JavaScript.

Namespaces can also merge with declared classes, functions, and enums.

We use cookies and similar technologies to enhance the quality of services, maintain statistics and adjust marketing content. You will find more information in the Cookies Policy.

By clicking OK you grant consent to processing of your personal data by us and our Trusted Partners with the purpose of maintain statistics and adjustment of the marketing content pursuant to the Privacy Policy. If you wish to not grant that consent and/or limit its extent click Settings.