TypeScript Glossary

Terminology helps us to describe a complex context and prevents us from confusion when talking about code. It plays an important role in understanding TypeScript and JavaScript.

As important as it is to know the control structures of a programming language, it is just as important to be able to name their contexts and surroundings. Using the right vocabulary is particularly effective in code reviews as it supports us to put our thoughts into words.


  1. Algorithms & Functions
    1. Algorithm
      1. Algorithm Characteristics
    2. Declarative Programming
    3. Imperative Programming
    4. Function Declaration
    5. Function Expression
    6. Arrow Function Expression
    7. Referentially Opaque Expressions
    8. Referentially Transparent Expressions
    9. Block Scope
    10. Function Scope
    11. Method vs. Function
    12. Deterministic Functions
    13. Identity Function
    14. Pure Functions
  2. Node.js
    1. Core Modules
  3. TypeScript
    1. Ambient Context
    2. Ambient Modules
    3. Array Destructuring
    4. Assertion Functions
    5. Assertion Signatures
    6. Boxed Types
    7. Collective Types
    8. Compiler
    9. Conditional Types
    10. Construct Signature
    11. Declaration Files
      1. Declaration vs. Definition
    12. Destructuring Assignment
    13. Discriminated Unions
    14. Downleveling
    15. Exports
    16. Function Argument
    17. Function Overloading
      1. Generics vs. Function Overloading
    18. Function Signature
    19. Function
    20. Generators
    21. Generics
    22. Imports
      1. Named Imports
      2. Default Imports
      3. Import Assertions
      4. Import Attributes
      5. Import Assignments
      6. Import Elision
    23. Interfaces
    24. Intersection Types
    25. Literal Types
      1. Literal Narrowing
    26. Lookup Types
    27. Mapped Types
    28. Module Augmentation
    29. Module
    30. Non-null Assertion Operator
    31. Non-primitive types
    32. Primitive Types
    33. String Literal Type
    34. Structural Typing
    35. Tagged template
    36. Template Literal Type
    37. Template Literal
    38. Top Types
    39. Transpiler
    40. Triple-Slash Directives
    41. Tuple Type
      1. Variadic Tuple Types
      2. Labeled Tuple Elements
      3. Tuples vs. Arrays
    42. Type Alias
    43. Type Annotation
    44. Type Argument Inference
    45. Type Argument
    46. Type Assertion
    47. Type Coercion
    48. Type Erasure
    49. Type Guards
      1. Type Guard with typeof
      2. Type Guard with in
      3. Type Guard with instanceof
      4. Custom Type Guard
    50. Type Predicates
    51. Type Inference
    52. Type Narrowing
    53. Type Variable
    54. Type Widening
    55. Union Types
      1. Literal Union Types
    56. Weak Types

Algorithms & Functions

Algorithm

An algorithm is a set of instructions to solve specific problems or to perform a computation. In TypeScript algorithms can be implemented with functions.

Algorithm Characteristics

Algorithms have defining characteristics such as:

Declarative Programming

Declarative programming is a programming paradigm that defines the desired state of an application without explicitly listing statements that must be executed. In a nutshell, declarative programming defines an application's execution from finish to start.

Example:

Imperative Programming

Imperative programming is a programming paradigm that uses statements to change an application's state. In a nutshell, imperative programming defines a set of instructions from start to finish.

Example:

Function Declaration

A function declaration gets hoisted and is written in the following way:

function myFunction(): number {
  return 1337;
}

Function Expression

A function expression is part of an assignment and does not get hoisted:

const myFunction = function (): number {
  return 1337;
};

Arrow Function Expression

Compact form of writing a function expression which got introduced in ES6:

const myFunction = (): number => 1337;

Referentially Opaque Expressions

An expression is referentially transparent when it cannot be replaced with its return value.

Example:

function today(): string {
  return new Date().toISOString();
}
 
const isoDate = today();

At the time of writing the execution of today() returned '2021-09-22T12:45:25.657Z'. This result will change over time, so we cannot replace const isoDate = today() with const isoDate = '2021-09-22T12:45:25.657Z' which makes this expression referentially opaque.

Referentially Transparent Expressions

An expression is referentially transparent when it can be replaced with its return value.

Example:

function sayHello(): string {
  return `Hello!`;
}
 
const message = sayHello();

The sayHello function always returns the same text, so we can safely replace our expression with const message = Hello! which makes it referentially transparent.

Block Scope

To enforce block-scoping in JavaScript, the let keyword can be used. It makes variables inaccessible from the outside of their blocks:

function myFunction(): void {
  if (true) {
    let myNumber = 1337;
  }
  // Will throw a `ReferenceError` because `myNumber` is not defined
  return myNumber;
}
 
console.log(myFunction()); // Causes an uncaught `ReferenceError`

Function Scope

By default, JavaScript is function scoped which means that variables are accessible within a function:

function myFunction(): void {
  if (true) {
    var myNumber = 1337;
  }
  // Variable `myNumber` can be accessed within the function although it is being used outside of its conditional if-block
  return myNumber;
}
 
console.log(myFunction()); // 1337

Method vs. Function

Sometimes the term "method" is used as a synonym for a "function". In JavaScript and TypeScript there is a clear distinction: When a function is a property of an object or class, then it is called a method. This regulation is made because functions can have a different value for this depending on their association with an object or class instance.

Function:

'use strict';
 
function someFunction(this: unknown): void {
  console.log(`I am a function and my this value is "undefined"`, this);
}
 
someFunction();

Method:

'use strict';
 
const person = {
  name: 'Benny',
  someMethod: function () {
    console.log(`I am a method and my this value is "object"`, this);
  },
};
 
person.someMethod();

If you turn an arrow function expression into a method, you will notice that the value of this switches to the global object:

'use strict';
 
const person = {
  name: 'Benny',
  someMethod: () => {
    console.log(`I am an arrow function and my this value is the global object.`);
  },
};
 
person.someMethod();

Deterministic Functions

A deterministic function will always produce the same output given the same input. This makes the output of a deterministic function predictable as it does not rely on a dynamic state.

Identity Function

An identity function returns the identical value that was given to it:

Example:

function identityFunction(text: string): string {
  return text;
}

Pure Functions

Pure functions are a subset of deterministic functions. A pure function always produces the same result given a particular input. In addition, it does not cause side effects by avoiding I/O operations like printing to the console or writing to the disk.

A pure function does not mutate its passed parameters and is referentially transparent.

Node.js

Core Modules

Core modules are defined within Node.js and can be identified by the node: prefix.

Example:

import assert from 'node:assert';
assert.equal('text', 'text');

TypeScript

Ambient Context

By default, the TypeScript compiler does not know in which runtime environment (for instance Node.js v16, Electron v16, Chrome v94) our code will be executed later. That's why we can help the compiler knowing that by defining an ambience / ambient context.

Example:

If you run your code in an environment where there is a "world" object that TypeScript does not know about, you can define that context using declare var:

declare var world: {
  name: string;
};
 
console.log(world.name);

Ambient Modules

A package that contains declaration files using the declare module syntax is called an ambient module. The main purpose of ambient modules is to provide typings for code that has been purely written in JavaScript, so that TypeScript can make use of it.

You can find a lot of ambient modules in the Definitely Typed repository which provides them under the @types namespace on the npm Registry. A very popularΒΉ example is the @types/node package which declares multiple modules like the buffer module:

buffer.d.ts
declare module 'buffer' {
  import { BinaryLike } from 'node:crypto';
  import { ReadableStream as WebReadableStream } from 'node:stream/web';
  export const INSPECT_MAX_BYTES: number;
  export const kMaxLength: number;
  export const kStringMaxLength: number;
  export const constants: {
    MAX_LENGTH: number;
    MAX_STRING_LENGTH: number;
  };
  export type TranscodeEncoding = 'ascii' | 'utf8' | 'utf16le' | 'ucs2' | 'latin1' | 'binary';
 
  // ...
}

If you plan to use some plain JavaScript code for testing, but you don't want to invest the time to write type definitions for it, you can use a shorthand ambient module declaration:

buffer.d.ts
declare module 'buffer';

ΒΉ 83,086,518 weekly downloads from 2022-12-06 to 2022-12-12

Array Destructuring

Extracting values from an array into distinct variables is called array destructuring.

Example:

const [bart, homer, marge] = ['Bart', 'Homer', 'Marge'];

Assertion Functions

Assertion functions are similar to user-defined type guards that allow developers to express their assumptions about the type and shape of input data. These functions can be used to check the type and value of variables. TypeScript's compiler will use this information at design-time to give fast feedback how a type should be used. An assertion function will also be executed at runtime to throw an error if a type check is not passed.

Example:

function assertString(input: unknown): asserts input is string {
  if (typeof input !== 'string') {
    throw new Error('Not a string!');
  }
}

Assertion Signatures

An assertion signature is a special syntax used to tell the compiler the expected outcome of an assertion. It is a constituent element of an assertion function.

Example:

asserts input is User

Boxed Types

A primitive data type has no methods or properties. to enable the use of methods on primitives, a mechanism called auto boxing comes into play.

Auto boxing involves automatically converting primitive data types like number, string, and boolean into their respective wrapper objects (Number, String, Boolean) when an object-specific property or method is accessed on them. This allows primitives to temporarily behave like objects with access to additional functionality.

Here's an example to illustrate auto boxing:

console.log((72).toFixed(2)); // 72.00

The boxed type Number can be also expressed as:

Number.prototype.toFixed.call(72, 2);

Collective Types

A collective type unites all literals of its kind. Collective types can be used to widen a type.

Example: The type number is a collective type of all integers as it includes different integer values:

const numberCollective: number = 72;

More collective types:

Compiler

Compilers transform high-level programming language code to low-level machine code or some low-level intermediate representation (e.g. Bytecode in Java).

Conditional Types

TypeScript's conditional types can be likened to JavaScript's conditional ternary operator. Depending on a condition followed by a question mark, the types evaluate to either a truthy or falsy condition.

Example:

type ConfigOption = 'custom' | 'default' | 'main';
 
type CustomConfig = { [key: string]: string };
 
type MainConfig = {
  cache: boolean;
  inputDir: string;
  outputDir: string;
  title: string;
};
 
type DefaultConfig = {
  inputDir: string;
  outputDir: string;
};
 
type FileOptions<T extends ConfigOption> = T extends 'custom'
  ? CustomConfig
  : T extends 'main'
    ? MainConfig
    : DefaultConfig;
 
const config: FileOptions<'main'> = {
  cache: true,
  inputDir: 'src',
  outputDir: 'dist',
  title: 'My Project',
};

Construct Signature

When a function in JavaScript is called with the new keyword, the function will be used as a constructor. In order for this behavior to be reflected in TypeScript, you must provide a construct signature for your constructor function:

Example:

interface CatConstructor {
  // Construct signature
  new (name: string): ICat;
}
 
interface ICat {
  name: string;
  meow(): string;
}
 
// Turning function into constructor function
const Cat = function (this: ICat, name: string) {
  this.name = name;
} as unknown as CatConstructor;
 
// Prototype-based programming
Cat.prototype.meow = function (): string {
  return 'Meow!';
};
 
const myCat = new Cat('Joey');
console.log(myCat.name); // "Joey"
console.log(myCat.meow()); // "Meow!"

Declaration Files

Declaration files (also known as .d.ts files) are used to provide type information for existing JavaScript code that doesn't have any type annotations.

When you import a JavaScript library, TypeScript may not be able to understand the types and functions defined in the JavaScript code. This can lead to issues with type safety and make it difficult to use the imported code in a TypeScript project.

Declaration files provide a way to address this issue by defining the types and functions for the JavaScript code so that TypeScript can understand them. They essentially tell TypeScript what the shape of the code is, without information about the concrete implementation.

Example:

index.d.ts
export function formToJSON(form: GenericFormData|GenericHTMLFormElement): object;
 
export function isAxiosError<T = any, D = any>(payload: any): payload is AxiosError<T, D>;
 
export function spread<T, R>(callback: (...args: T[]) => R): (array: T[]) => R;
 
export function isCancel(value: any): value is Cancel;
 
export function all<T>(values: Array<T | Promise<T>>): Promise<T[]>;

Declaration vs. Definition

A declaration introduces an identifier (such as a variable or a function) to the compiler, while a definition provides the implementation or value for that identifier.

Example:

// Here is a declaration:
type Greet = (name: string) => void;
 
// Here is a definition:
function greet(name: string): void {
  console.log(`Hello, ${name}!`);
}

Destructuring Assignment

Destructuring assignments allow to extract data from arrays and objects into individual variables. It is a shorthand for creating a new variable for each value of an array or object.

Example:

// Destructuring an object
const object = {
  age: 35,
  firstName: 'Benny',
};
 
const { age, firstName } = object;
 
console.log(`${firstName} is ${age} years old.`);
 
// Destructuring an array
const array = [300, 500];
 
const [min, max] = array;
 
console.log(`The range is between ${min} and ${max}.`);

Discriminated Unions

Discriminated Unions (or tagged union types) can narrow a type based on a shared property.

In the following example, the type of Dog and Person have a shared property called type. Depending on the value of this property, TypeScript can narrow down the type from Dog | Person to either Dog or Person:

type Dog = {
  age: number;
  name: string;
  bark: () => void;
  type: 'dog';
};
 
type Person = {
  age: number;
  name: string;
  shout: () => void;
  type: 'person';
};
 
function makeNoise(dogOrPerson: Dog | Person): void {
  switch (dogOrPerson.type) {
    case 'dog':
      // Type is narrowed down to "Dog", so we can "bark":
      dogOrPerson.bark();
      break;
    case 'person':
      // Type is narrowed down to "Person", so we can "shout":
      dogOrPerson.shout();
      break;
  }
}

Because this allows the type to be discriminated, this technique is referred to as discriminating unions. This concept also exists in F# (Discriminated Unions in F#).

If multiple properties are shared, it is recommended to create a base type from which your other types can inherit:

type Creature = {
  age: number;
  name: string;
};
 
type Dog = Creature & {
  bark: () => void;
  type: 'dog';
};
 
type Person = Creature & {
  shout: () => void;
  type: 'person';
};
 
type CreatureUnion = Dog | Person;
 
function makeNoise(dogOrPerson: CreatureUnion): void {
  switch (dogOrPerson.type) {
    case 'dog':
      // Type is narrowed down to "Dog", so we can "bark":
      dogOrPerson.bark();
      break;
    case 'person':
      // Type is narrowed down to "Person", so we can "shout":
      dogOrPerson.shout();
      break;
  }
}

Downleveling

Downleveling in TypeScript refers to the process of transpiling source code to an older version of ECMAScript/JavaScript.

Exports

A module export is a mechanism used to make functions, classes, variables, or other entities available for use in other modules. Exporting allows you to encapsulate functionality into separate modules and make them reusable by importing them in other files or projects.

Function Argument

What you pass to a function is called an argument. What the function expects to receive is called a parameter.

In summary, the parameter is what the function expects to receive, and the argument is the actual value that is passed to satisfy that expectation.

Function Overloading

With function overloading you can define multiple functions with the same name but with different input and output types.

It is a useful technique when there are a limited number of possible input types for a function and the return type depends on the input type. When calling an overloaded function, the TypeScript compiler will look for the function signature that best matches the input parameters and return the corresponding type. This saves the users of your function from having to specify the type argument and improves the Developer Experience (DX).

Example:

// Function Overload #1
function combine(x: number, y: number): number[];
// Function Overload #2
function combine(x: string, y: string): string[];
// Function Implementation
function combine(x: number | string, y: number | string) {
  return [x, y];
}
 
combine('1', '2'); // string[] will be returned
combine(1, 2); // number[] will be returned

Video Tutorial:

Generics vs. Function Overloading

When the possible types are many and you don't know exactly what type your users put in, then Generis are better suited. Generics allow you to specify the type argument which can make it easier to work with functions that can handle a wide range of input types.

Nevertheless, there are situations in which function overloads are more suitable. For example, to make functions that use operators like + reusable (learn how).

Function Signature

A function signature defines the shape or structure of a function in TypeScript. It specifies the parameter list, the return type, and optionally the this type of a function. A function signature can be also expressed with a function type.

Example:

type MyFunctionSignature = (a: number, b: number) => number;
 
const add: MyFunctionSignature = (a, b) => {
  return a + b;
};

Function

A function consists of several building blocks that define its anatomy. Here's a breakdown of the different parts:

  1. Function Name: This is the identifier used to call the function.
  2. Function Signature: Defines the shape of the function, including parameter list, this context and return type.
  3. Function Body: This is the block of code enclosed within curly braces ({}). It contains the instructions and statements that define the behavior of the function. The code within the function body is executed when the function is called.

Generators

Generators are functions that can return multiple successive values (called yields). Generator functions are declared using the "function" keyword followed by an asterisk (function*). When a generator function is called, it returns an object which implements the iterable protocol and iterator protocol.

The iterator protocol defines a next function that can be used to retrieve the latest value of the generator function.

The iterable protocol allows the objects to be looped over in a for...of construct. To align with the iterable protocol, an object must define an iterator function in a property called Symbol.iterator.

Example:

function* generateNumbers(amount: number) {
  const data = Array.from({ length: amount }, (_, index) => index + 1);
 
  for (const item of data) {
    yield item;
  }
}
 
// Initializing the generator
const numberGenerator = generateNumbers(10);
 
// Iterating through the generator
for (const number of numberGenerator) {
  console.log(number);
}

Generics

Generics allow you to create templates for your types.

Example:

By using Generics you can relate an input parameter to an output parameter. The following code sets the type variable (T) to number when calling the generic function combine:

function combine<T>(a: T, b: T): T[] {
  return [a, b];
}
 
combine<number>(1, 2); // [1, 2]

TypeScript supports type argument inference when passing the input values:

function combine<T>(a: T, b: T): T[] {
  return [a, b];
}
 
combine(1, 2); // [1, 2]

You can also specify multiple type variables:

function combine<X, Y>(a: X, b: Y): (X | Y)[] {
  return [a, b];
}
 
combine(1, '2'); // [1, "2"]

Default type variables are supported as well:

function combine<X, Y = string>(a: X, b: Y): (X | Y)[] {
  return [a, b];
}
 
// It became optional to pass a type argument for `Y`:
combine<number>(1, '2'); // [1, "2"]

It is also possible to enforce a certain structure on your generic types using the extends keyword:

function combine<T extends { name: string }>(a: T, b: T): T[] {
  return [a, b];
}
 
combine({ name: 'Benny', type: 'Pet' }, { name: 'Sofia' });

Generics also work in classes:

class KeyValuePair<Value> {
  public key: string | undefined;
  public value: Value | undefined;
}

Imports

A module import is a mechanism used to bring functionality from one TypeScript code into another TypeScript code. This feature allows you to organize your code into smaller, manageable pieces and facilitates code reuse and maintenance.

For clarity, we will use the term "provider" to refer to someone exporting functionality (such as classes, functions, variables, etc.), and the term "consumer" for someone importing functionality.

When code is being exported, it can be imported in the following ways.

Named Imports

A named import refers to the concept of importing functionality that was given a specific name by the provier. In such cases you have to use the provided name when importing code.

Example:

import { add, subtract } from './math';

Default Imports

When you write a default import, you can use any name after "import" and still get the same default export:

import somenameyoulike from './math';

Import Assertions

Import assertions in TypeScript allow to specify additional metadata about the modules being imported.

Example:

import marketData from '../fixtures/2023-07-SHOP-USD.json' assert { type: 'json' };

Note: The import assertion syntax and its assert clause (assert { ... }) have been introduced in TypeScript 4.5 and got deprecated with the release of Import Attributes in TypeScript 5.3 (source).

Import Attributes

Import Attributes have been emerged from the TC39 Import Attributes proposal.

TypeScript 5.3 supports import attributes with the following syntax:

import marketData from '../fixtures/2023-07-SHOP-USD.json' with { type: 'json' };

Import Assignments

Import assignments are typical in TypeScript when importing CommonJS modules. An import assignment looks like this:

import Clipboard = require('clipboard');

When using ECMAScript modules the import assignments cannot be used and need to be transformed. If the CommonJS module has a default export, then it can be imported the following way:

import { default as Clipboard } from 'clipboard';

Import Elision

In TypeScript, "import elision" refers to the behavior of analyzing imports and removing them from the emitted JavaScript code if they are only used for type checking and have no runtime impact. This optimization process aims to eliminate unnecessary imports and optimize the output JavaScript code.

The compiler option verbatimModuleSyntax simplifies the rules for import elision as it keeps imports and exports without a type modifier intact. Imports and exports with the type modifier will be dropped completely ensuring a lean and predictable outcome.

Interfaces

An interface is a way to define a contract for the shape of an object. It specifies the names and types of properties and methods that an object must have to be considered an instance of that interface.

Interfaces can be used to specify the shape of objects, define the structure of classes, and describe the signatures of functions.

Example:

interface Calculator {
  (x: number, y: number): number;
}
 
interface ExtendedCalculator extends Calculator {
  (x: string, y: string): number;
}
 
const add: Calculator = (a, b) => a + b;
const advancedAdd: ExtendedCalculator = (a, b) => {
  return parseInt(`${a}`, 10) + parseInt(`${b}`, 10);
};
 
console.log(add(1000, 337)); // 1337
console.log(advancedAdd(1000, 337)); // 1337
console.log(advancedAdd('1000', '337')); // 1337

Interfaces are compile-time constructs, meaning that the TypeScript compiler does not generate any JavaScript code for interfaces at runtime.

Best Practice:

Interfaces are considered to be the better version of "types" as they have improved performance over intersection types (source), allow declaration merging and support module augmentation.

Intersection Types

An intersection type combines multiple types into a single type.

Example:

type User = {
  name: string;
};
 
type Address = {
  city: string;
  country: string;
};
 
// Intersection Type "Customer"
type Customer = User & Address;
 
const benny: Customer = {
  city: 'Berlin',
  country: 'Germany',
  name: 'Benny',
};

Literal Types

A literal type is a more specific subtype of a collective type. A literal type can represent an exact value, such as a specific number or string, instead of a general value that can be any number or string.

const numberLiteral: 72 = 72;
const stringLiteral: 'Benny' = 'Benny';
const booleanLiteral: true = true;
const objectLiteral = {
  age: 35,
  name: 'Benny',
};
 
enum ConnectionState {
  OFF,
  ON,
}
 
const enumLiteral = ConnectionState.OFF;

Literal Narrowing

Narrowing refers to the process of reducing the set of possible values that a variable can hold. TypeScript applies literal narrowing, when declaring a variable with the const keyword:

// Type of "text" is "Hello, World!" (string literal)
const text = 'Hello, World!';

On the other hand, when using the let keyword, TypeScript will infer a collective type such as number for the variable:

// Type of "text" is "string" (collective type)
let text = 'Hello, World!';

Lookup Types

A lookup type, also referred to as indexed access type, is a way to retrieve the type of a property of an object by its name or key. This is useful when you want to write code that works with dynamic property names, where the name of the property is not known until runtime.

The syntax for a lookup type is as follows: Type[Key], where Type is the type of the object and Key is the name of the property being looked up.

Example:

type Person = {
  address: {
    city: string;
    country: string;
  };
  name: {
    firstName: string;
    lastName: string;
  };
};
 
// Lookup types "Address" and "Name"
type Address = Person['address'];
 
type Name = Person['name'];
 
// Lookup types in action!
function getProperty(property: keyof Person, person: Person): Address | Name {
  return person[property];
}

Mapped Types

A mapped type is derived from an existing type by modifying the properties' names or accessibility (e.g. readonly) in the new type.

Example:

// Existing Type
type User = {
  age: number;
  firstName: string;
  lastName: string;
};
 
// Mapped Type
type PartialUser = {
  [P in keyof User]?: User[P];
};

Mapped types can be also used with template literals. Template literals are a powerful feature of TypeScript that allow you to create strings that contain expressions:

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
 
type EndpointPaths = `/${string}` | `/${string}/${string}`;
 
type ApiEndpoints = { [K in `${HttpMethod} ${EndpointPaths}`]: string };
 
const myApi: ApiEndpoints = {
  'GET /users': '/api/users',
  'POST /users': '/api/users',
  'PUT /users/123': '/api/users/123',
  'DELETE /users/123': '/api/users/123',
};

More about mapped types ➝

Module Augmentation

Module Augmentation is the process of extending type definitions by merging declarations from an existing module with your own type definitions. It is particularly useful when you want to modify interfaces from external packages. Such cases occur when type definitions from external packages are broken and need to be patched.

Example:

In the following example the existing definition for the Configuration from the webpack module gets extended by a property called devServer:

import type { Configuration } from 'webpack';
 
declare module 'webpack' {
  interface Configration {
    devServer: {
      open: boolean;
    };
  }
}

Video Tutorial:

Module

In TypeScript a module is a file with at least one top level import or export statement.

Non-null Assertion Operator

You can use the "non-null assertion operator" to tell the compiler that you're certain that an expression won't be null or undefined. This operator is represented by an exclamation mark (!) placed after the expression.

In the code snippet below, name! tells the TypeScript compiler to assume that the name property is not null or undefined. This allows to access the length property without the compiler raising an error:

function getNameLength(user: { name?: string }): number {
  return user.name!.length;
}

Synonyms: Definite Assignment Assertions

Non-primitive types

All non-primitive data types, also referred to as complex types, are passed by reference:

Primitive Types

All primitive data types are passed by value:

  1. bigint
  2. boolean
  3. null
  4. number
  5. string
  6. symbol
  7. undefined

TypeScript's type system additional has the following primitives:

  1. unknown

String Literal Type

A string literal is a specific string (e.g. "click") and a string literal type is the type describing a specific string:

const action: 'click' = 'click';

Structural Typing

Structural typing defines a type system where types are defined based on the structure of the data rather than explicit declarations. If two types have the same set of properties they are considered to be of the same type, even if they were defined separately and given different names.

Example:

type Point = {
  x: number;
  y: number;
};
 
type Vector = {
  x: number;
  y: number;
  z: number;
};
 
function printPoint(point: Point) {
  console.log(`Point coordinates: (${point.x}, ${point.y})`);
}
 
const vector: Vector = {
  x: 4,
  y: 3,
  z: 2,
};
 
printPoint(vector);

Even though Point and Vector are different types, they are compatible. We can pass a Vector object where a Point is expected, because it has a similar structure and matches all required properties of a Point.

Tagged template

import gql from 'graphql-tag';
 
const query = gql`
  {
    user(id: 5) {
      firstName
      lastName
    }
  }
`;

Template Literal Type

A template literal type is a combination of Template literal and string literal type. It is capable of resolving string interpolations within a template string to use it for strong typing:

type Action = 'click';
 
type ClickEvent = `${Action}Event`;
 
const myEvent: ClickEvent = 'clickEvent';

Template Literal

A template literal is a literal which has placeholders for variables (i.e. ${placeholder}), so it can act as a template for texts:

const templateLiteralString = `1 + 1 = ${1 + 1}`;

Top Types

The top type, also known as the universal type, refers to a type that serves as the parent or supertype for all other types within a specific type system. In TypeScript, the types any and unknown are considered top types as they encompass all other types in the type system.

Transpiler

Transpilers transform high-level programming language code (e.g. TypeScript) into another high-level programming language code (e.g. JavaScript).

Triple-Slash Directives

Triple-slash directives are special comments that provide instructions to the TypeScript compiler. These directives begin with three slashes (///) and are placed at the top of a file or just before a declaration to control the compilation process.

The most common use of triple-slash directives is to reference external dependencies or declaration files. For example, the /// <reference path="..." /> directive is used to reference external TypeScript declaration files (*.d.ts) that provide type information for libraries or modules used in the current file.

Triple-slash directives can also be used to configure certain compiler features. For instance, the AMD module directive (/// <amd-module />) specifies that the current file is an AMD module and allows passing an optional module name to the compiler.

It's important to note that starting from TypeScript 3.0, the preferred way to manage dependencies and module resolution is through import statements. Triple-slash directives are mainly used for legacy scenarios or compatibility reasons.

Tuple Type

A tuple is a sub-type of an array. It is similar to an array in that it can hold elements of different data types, but it differs in that it has a finite number of elements.

Example:

type MyTupleType = [string, number];
const benny: MyTupleType = ['Benny', 34];

Variadic Tuple Types

Variadic tuple types are a feature in TypeScript that allow an indefinite number of elements. The syntax for defining a variadic tuple type is to use the rest syntax (...) to condense multiple elements into one:

type MyVariadicTupleType = [string, ...number[]];
 
const array: MyVariadicTupleType = ['prime numbers', 2, 3, 5, 7, 11];

In the code example above, a benefit of using a variadic tuple type over an array (Array<string | number>) is that it enforces that the first element of the tuple must be of type string.

Labeled Tuple Elements

As of TypeScript 4.0 you can assign labels to the elements of a tuple type for improved readability:

type MyTupleType = [name: string, age: number];
const benny: MyTupleType = ['Benny', 34];

Tuples vs. Arrays

A tuple has a fixed number of elements and arrays have a variable number of elements.

Example:

// Tuple, fixed size of elements
const id: [number] = [15];
 
// Array
const ids: number[] = [1, 2, 3];
// More and more elements can be pushed
ids.push(4);

Tuple types are denoted by square brackets placed outside of the data type (e.g. [number]), while array types have square brackets following the data type (e.g. number[]):

const tuple: [string, number] = ['Benny', 34];
const array: (string | number)[] = ['Benny', 34];

Type Alias

A type alias is a way to create a new name for an existing type.

Example:

To express that a string is a user ID, you can create a UserID type alias:

type UserID = string;

Type Annotation

A type annotation is when you explicitly define the type that a variable can take on:

const x: number = 10;

Video Tutorial:

Type Argument Inference

TypeScript can infer types but also type arguments. This usually happens when the arguments of your generic functions are connected to the generic type of your function. In this case TypeScript can infer the type of your type variable by inspecting the input values of your function call:

function yourGenericFunction<T>(input: T[]): number {
  return input.length;
}
 
/** By inspecting the input of this function call, TypeScript will infer `yourGenericFunction<string>`. */
yourGenericFunction(['Benny']);

Type Argument

In the following code, <string> is the type argument (sometimes called type parameter):

const array = new Array<string>();

Type Assertion

A type assertion is a way to tell the compiler the type of a variable when TypeScript is unable to infer it automatically:

const text = 'Hello, World!' as string;

It is important to be cautious with type assertions because they can potentially lead to dangerous errors. This is because type assertions override TypeScript's type inference, and if used incorrectly, may result in assigning an incorrect type to a variable:

// πŸ’€: "Hello, World!" is NOT "Something else!"
const text = 'Hello, World!' as 'Something else!';

Best Practice:

Type assertions can be particularly handy when writing test code. They enable you to test whether your TypeScript code can handle incorrect input by asserting a correct type to incorrect data. This enables you to pass the incorrect data to your code under test, which would otherwise be disallowed by TypeScript's type system.

When you need to override TypeScript's type inference, it is recommended to use an assertion function instead of a regular type assertion.

Video Tutorial:

Related:

Type Coercion

When a type is implicitly turned into another type, it's called type coercion.

Example:

The + operator coerces its operands, so it can work properly. When adding a number type and a string type, the number type is turned (coerced) into a string:

const message = 'The answer is: ' + 1337; // `1337` is coerced to a string

Type Erasure

During compilation, TypeScript removes type annotations, interfaces, type aliases, and other type constructs from the output. This effect is called "Type Erasure".

Example:

main.ts
const myName = 'Benny' as 'Rudolf';

The type assertion as 'Rudolf' only exists in TypeScript's type system and will be removed when the code is transpiled to JavaScript:

main.js
'use strict';
const myName = 'Benny';

Type Guards

A type guard is a boolean expression that does a runtime check to guarantee the type in a certain scope.

Built-in Type Guards

Type Guard with typeof

A typeof type guard can protect primitive types.

function add1000(input: string | number): number {
  if (typeof input === 'number') {
    // Type guard helped us to detect a "number" (saves us from using "parseInt")
    return input + 1000;
  } else {
    return parseInt(input, 10) + 1000;
  }
}

When dealing with complex types, a typeof type guard may not be very useful since the type of a complex type will always be "object" (see here). In such cases, an in type guard would be more effective.

Type Guard with in

type Dog = {
  name: string;
  bark: () => void;
  run: () => void;
};
 
type Person = {
  name: string;
  shout: () => void;
  walk: () => void;
};
 
function makeNoise(dogOrPerson: Dog | Person): void {
  if ('bark' in dogOrPerson) {
    // Type guard helped us to detect a "Dog"
    dogOrPerson.bark();
  } else {
    dogOrPerson.shout();
  }
}

While the in type guard is effective for checking plain objects, it is recommended to use the instanceof type guard when checking a class.

Type Guard with instanceof

class Dog {
  name?: string;
  bark = () => {};
  run = () => {};
}
 
class Person {
  name?: string;
  shout = () => {};
  walk = () => {};
}
 
function makeNoise(dogOrPerson: Dog | Person): void {
  if (dogOrPerson instanceof Dog) {
    dogOrPerson.bark();
    dogOrPerson.run();
  } else {
    dogOrPerson.shout();
    dogOrPerson.walk();
  }
}

Custom Type Guard

function isAxiosError(error: unknown): error is AxiosError {
  if (error && typeof error === 'object' && 'isAxiosError' in error) {
    return true;
  }
  return false;
}

Type Predicates

A type predicate is a syntax construct used to define the return type of a type guard. It allows the type guard to assert a specific type to its input. Here is an example of a type predicate: error is AxiosError

Type Inference

When there is no explicit type annotation then TypeScript will infer the type for you:

/** TypeScript infers `number` because "x" is initialized with a number and can be reassigned. */
let x = 10;
/** TypeScript infers `10` because "x" is a constant and cannot be reassigned. */
const x = 10;
/** TypeScript infers an array of `(string | number)` value types. */
const x = [10, '11'];

Type Narrowing

Refining wide types to more specific types is called type narrowing.

Concept:

Type Widening

Type Variable

A type variable is the placeholder for a generic type in your generic code:

function yourGenericFunction<MyTypeVariable>(input: MyTypeVariable[]): number {
  return input.length;
}

Type variables are written by using the angle brackets and defining a name for the variable (e.g. <T>). This construct is often referred to as the diamond operator because the angle brackets look like a diamond (<>, πŸ’Ž).

Type Widening

You widen a type when you assign a value to a variable where the variable has a supertype of the value.

// The value 72 is assigned to the wide type of "number"
const myVariable: number = 72;

Concept:

Type Widening

Union Types

A union type lets you specify a range of possible types for a value:

type MyUnionType = string | number;
 
const myName: MyUnionType = 'Benny';
const myAge: MyUnionType = 34;

It is called a union because it unites the amount of possible types. The term union comes from set theory where it is used when two (or more) sets are combined.

Union types become pretty powerful in the context of discriminated Unions.

Literal Union Types

Literal union types can be useful when you want to represent a set of specific values (literals). For example, consider this string literal union type:

type EventType = 'error' | 'info' | 'warn';

In conjunction with type guards a literal union type can make your code more expressive and type-safe.

Weak Types

If all properties of your type or interfaces are optional, then this type or interface is considered to be weak:

Example:

interface User {
  age?: number;
  firstName?: string;
  lastName?: string;
}