Theme

Type Operations

When we have a variable, we can perform operations on it. For example, we can use * to multiply two numbers, use .split to split a string into an array.

If we have a type, we can perform operations on it too. We’re going to discuss few of them in this lesson.

Type Alias

Type alias is the naming of type, so you can reuse it.

// instead of
let w: string | number = 5;
let x: string | number = 'five';

// create a type alias to reuse
type StringOrNumber = string | number;
let y: StringOrNumber = 5;
let z: StringOrNumber = 'five';

Type alias is not limited to primitive, you can use it to name object type as well.

type Job = {
  name: string;
  salary: number;
};

const a: Job = {
  name: 'programmer',
  salary: 0,
};

type NumberOrStringArray = Array<number | string>;

const items: NumberOrStringArray = [3, 'three', 4, 5];

Most of the thing that you can do using interface can be done using type, so I usually just use type whenever possible.

Intersection & Union

/**
 * Union types
 * Sometimes we have a type that can be one of several things
 */

interface WithPhoneNumber {
  name: string;
  phone: number;
}

interface WithEmail {
  name: string;
  email: string;
}

let contactInfo: WithPhoneNumber | WithEmail =
  Math.random() > 0.5
    ? {
        // we can assign it to a WithPhoneNumber
        name: 'Malcolm',
        phone: 60174444444,
      }
    : {
        // or a WithEmail
        name: 'Malcolm',
        email: 'malcolm@example.com',
      };

contactInfo.name; // NOTE: we can only access the .name property  (the stuff WithPhoneNumber and WithEmail have in common)

/**
 * Intersection types
 */
let otherContactInfo: WithPhoneNumber & WithEmail = {
  //   we _must_ initialize it to a shape that's asssignable to WithEmail _and_ WithPhoneNumber
  name: 'Malcolm',
  email: 'malcolm@example.com',
  phone: 601744444444,
};

otherContactInfo.name; // NOTE: we can access anything on _either_ type
otherContactInfo.email;
otherContactInfo.phone;

Takeaways:

  1. Union is like OR operator. In mathematics, union is represented by two overlapping circles with both of them covered.
  2. Intersection is like AND operator. In mathematics, intersection is presented by two overlapping circles with the overlapped parts covered.

Venn diagram showing comparisons of union and intersections

Accessing Type of an Object Type Property

Sometimes you want to extract out the type of an Object Type and use it somewhere. Consider the example below:

type PersonDetails = {
  name: string;
  address: {
    unit: string;
    streetOrBuilding: string;
    street: string;
  };
};

let oldDetails: PersonDetails = {
  name: 'Malcolm',
  address: {
    unit: '12A',
    streetOrBuilding: 'Jalan Besar',
    street: 'Jalan',
  },
};

let newAddress: {
  unit: string;
  streetOrBuilding: string;
  street: string;
} = {
  unit: '13A',
  streetOrBuilding: 'Bangunan Duta',
  street: 'Jalan Duta',
};

let updatedDetails: PersonDetails = {
  ...oldDetails,
  address: newAddress,
};

Note that we have duplicated the address type in two places. We can remove that duplication by creating a type for the address:

// [!code highlight:6]
type 
type Address = {
    unit: string;
    streetOrBuilding: string;
    street: string;
}
Address
= {
unit: stringunit: string; streetOrBuilding: stringstreetOrBuilding: string; street: stringstreet: string; }; type
type PersonDetails = {
    name: string;
    address: Address;
}
PersonDetails
= {
name: stringname: string; address: Addressaddress:
type Address = {
    unit: string;
    streetOrBuilding: string;
    street: string;
}
Address
; // [!code highlight]
}; let let oldDetails: PersonDetailsoldDetails:
type PersonDetails = {
    name: string;
    address: Address;
}
PersonDetails
= {
name: stringname: 'Malcolm', address: Addressaddress: { unit: stringunit: '12A', streetOrBuilding: stringstreetOrBuilding: 'Jalan Besar', street: stringstreet: 'Jalan', }, }; // [!code highlight:2] let let newAddress: AddressnewAddress:
type Address = {
    unit: string;
    streetOrBuilding: string;
    street: string;
}
Address
= {
unit: stringunit: '13A', streetOrBuilding: stringstreetOrBuilding: 'Bangunan Duta', street: stringstreet: 'Jalan Duta', }; let let updatedDetails: PersonDetailsupdatedDetails:
type PersonDetails = {
    name: string;
    address: Address;
}
PersonDetails
= {
...let oldDetails: PersonDetailsoldDetails, address: Addressaddress: let newAddress: AddressnewAddress, };

However, sometimes this is not possible because you don’t have control over type definition (e.g. type from library) so you can’t just refactor the type definition.

The solution is to extract the type of the property like this:

type PersonDetails = {
  name: string;
  address: {
    unit: string;
    streetOrBuilding: string;
    street: string;
  };
};

let oldDetails: PersonDetails = {
  name: 'Malcolm',
  address: {
    unit: '12A',
    streetOrBuilding: 'Jalan Besar',
    street: 'Jalan',
  },
};
// [!code highlight:2]
let newAddress: PersonDetails['address'] = {
  unit: '13A',
  streetOrBuilding: 'Bangunan Duta',
  street: 'Jalan Duta',
};

let updatedDetails: PersonDetails = {
  ...oldDetails,
  address: newAddress,
};

Differentiating Type and Runtime: Declaration Space

One of the implicit understanding of TypeScript is that there are two types of declaration: type declaration and variable declaration.

Following are a few type declarations:

interface Animal {
  name: string;
  greet: () => void;
}

type Person = {
  firstName: string;
  talk: () => void;
};

Which means you can use them as type annotation:

const animal: Animal = {
  name: 'dog',
  greet: () => {
    console.log(`Woff! Woff!`);
  },
};

const me: Person = {
  firstName: 'Malcolm',
  talk: () => {
    console.log(`Hello!`);
  },
};

But you can’t use them as variable:

const person = Person; // Error

Similarly, when you declare a variable, you can’t use it as type annotation:

const person = {
  name: '',
  talk: () => {
    console.log(`Sawatika`);
  },
};

const me: person = {
  name: '',
  talk: () => {
    console.log(`Sawatika`);
  },
};

If what you want is to get the type of a variable and apply it to another variable, you can use the typeof keyword:

const person = {
  name: '',
  talk: () => {
    console.log(`Sawatika`);
  },
};

const me: typeof person = {
  name: '',
  talk: () => {
    console.log(`Sawatika`);
  },
};

Escape Hatch: Type Assertion

Sometimes, some values are really dynamic that there are no ways for TypeScript to do type-checking for you.

Consider the following example:

const const profileImage: Element | nullprofileImage = var document: Document
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/document)
document
.ParentNode.querySelector<Element>(selectors: string): Element | null (+4 overloads)
Returns the first element that is a descendant of node that matches selectors. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/querySelector)
querySelector
('#profile');
var console: Console
The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)
console
.Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)
Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.
@sincev0.1.100
log
(profileImage.src); // Error because `src` property may not be there
Property 'src' does not exist on type 'Element'.
'profileImage' is possibly 'null'.

If we really sure that the document.querySelector call returns an image, we can make a type assertion to tell TypeScript:

const const profileImage: Element | nullprofileImage = var document: Document
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/document)
document
.ParentNode.querySelector<Element>(selectors: string): Element | null (+4 overloads)
Returns the first element that is a descendant of node that matches selectors. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/querySelector)
querySelector
('#profile');
var console: Console
The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)
console
.Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)
Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.
@sincev0.1.100
log
((const profileImage: Element | nullprofileImage as HTMLImageElement).HTMLImageElement.src: string
The address or URL of the a media resource that is to be considered. [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLImageElement/src)
src
); // [!code highlight]

Note that type assertion does nothing in runtime; if the document.querySelector calls above returns a div instead of a img, the call could cause error.