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

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. 1. Algorithms & Functions
    1. 1.1. Algorithm
    2. 1.2. Declarative Programming
    3. 1.3. Imperative Programming
    4. 1.4. Function Declaration
    5. 1.5. Function Expression
    6. 1.6. Arrow Function Expression
    7. 1.7. Referentially Opaque Expressions
    8. 1.8. Referentially Transparent Expressions
    9. 1.9. Block Scope
    10. 1.10. Function Scope
  2. 2. Method vs. Function
    1. 2.1. Deterministic Functions
    2. 2.2. Identity Function
    3. 2.3. Pure Functions
  3. 3. Node.js
    1. 3.1. Core Modules
  4. 4. TypeScript
    1. 4.1. Ambient Context
    2. 4.2. Ambient Modules
    3. 4.3. Array Destructuring
    4. 4.4. Assertion Functions
    5. 4.5. Assertion Signatures
    6. 4.6. Collective Types
    7. 4.7. Compiler
    8. 4.8. Construct Signature
    9. 4.9. Declaration Files
    10. 4.10. Destructuring Assignment
    11. 4.11. Discriminated Unions
    12. 4.12. Function Overloading
    13. 4.13. Function Signature
    14. 4.14. Generics
    15. 4.15. Interfaces
    16. 4.16. Intersection Types
    17. 4.17. Literal Types
    18. 4.18. Lookup Types
    19. 4.19. Mapped Types
    20. 4.20. Module Augmentation
    21. 4.21. Module
    22. 4.22. Non-primitive types
    23. 4.23. Primitive Types
    24. 4.24. String Literal Type
    25. 4.25. Tagged template
    26. 4.26. Template Literal Type
    27. 4.27. Template Literal
    28. 4.28. Transpiler
    29. 4.29. Triple-Slash Directives
    30. 4.30. Tuple Type
    31. 4.31. Type Alias
    32. 4.32. Type Annotation
    33. 4.33. Type Argument Inference
    34. 4.34. Type Argument
    35. 4.35. Type Assertion
    36. 4.36. Type Guards
    37. 4.37. Type Predicates
    38. 4.38. Type Inference
    39. 4.39. Type Narrowing
    40. 4.40. Type Variable
    41. 4.41. Type Widening
    42. 4.42. Union Types
    43. 4.43. 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:

1
2
3
function myFunction(): number {
return 1337;
}

Function Expression

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

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

Arrow Function Expression

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

1
const myFunction = (): number => 1337;

Referentially Opaque Expressions

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

Example:

1
2
3
4
5
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:

1
2
3
4
5
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:

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
'use strict';

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

someFunction();

Method:

1
2
3
4
5
6
7
8
9
10
'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:

1
2
3
4
5
6
7
8
9
10
'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:

1
2
3
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:

1
2
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:

1
2
3
4
5
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
1
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:

1
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:

1
2
3
4
5
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:

1
asserts input is User

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:

1
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).


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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
// 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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#).


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:

1
2
3
4
5
6
7
8
9
10
11
// 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: https://stackoverflow.com/questions/59802954/


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:

1
2
3
4
5
type MyFunctionSignature = (a: number, b: number) => number;

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

In TypeScript, 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.

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:

1
2
3
4
5
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:

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

combine(1, 2); // [1, 2]

You can also specify multiple type variables:

1
2
3
4
5
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:

1
2
3
4
5
6
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:

1
2
3
4
5
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:

1
2
3
4
class KeyValuePair<Value> {
public key: string | undefined;
public value: Value | undefined;
}

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 sub-type 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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:

1
2
// 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:

1
2
// 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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:

1
2
3
4
5
6
7
8
9
10
11
// 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:

1
2
3
4
5
6
7
8
9
10
11
12
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:

1
2
3
4
5
6
7
8
9
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-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:

1
const action: 'click' = 'click';

Tagged template

1
2
3
4
5
6
7
8
9
10
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:

1
2
3
4
5
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:

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

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:

1
2
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:

1
2
3
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:

1
2
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:

1
2
3
4
5
6
7
// 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[]):

1
2
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:

1
type UserID = string;

Type Annotation

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

1
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:

1
2
3
4
5
6
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):

1
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:

1
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:

1
2
// 💀: "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 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.

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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

1
2
3
4
5
6
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:

1
2
/** TypeScript infers `number` because "x" is initialized with a number and can be reassigned. */
let x = 10;
1
2
/** TypeScript infers `10` because "x" is a constant and cannot be reassigned. */
const x = 10;
1
2
/** 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 Variable

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

1
2
3
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.

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

Concept:


Union Types

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

1
2
3
4
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.

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:

1
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:

1
2
3
4
5
interface User {
age?: number;
firstName?: string;
lastName?: string;
}