skip to content
Home Image

TypeScript

/ 30 min read

Table of Contents

Introduction

Q. What is TypeScript?

TypeScript is a syntactic superset of JavaScript which adds static typing.

This basically means that TypeScript adds syntax on top of JavaScript, allowing developers to add types.

Q. Why should I use TypeScript?

JavaScript is a loosely typed language.

It can be difficult to understand what types of data are being passed around in JavaScript.

In JavaScript, function parameters and variables don’t have any information!

So developers need to look at documentation, or guess based on the implementation.

TypeScript allows specifying the types of data being passed around within the code, and has the ability to report errors when the types don’t match.

For example, TypeScript will report an error when passing a string into a function that expects a number.

JavaScript will not.

TypeScript Simple Types

The most basic types in TypeScript are called primitives.

These types form the building blocks of more complex types in your applications.

TypeScript includes all JavaScript primitives plus additional type features.

Here are the five primitive types you’ll use most often:

  1. Boolean

Represents true/false values.

Used for flags, toggles, and conditions.

let isActive: boolean = true;
let hasPermission = false; // TypeScript infers 'boolean' type
  1. Number

Represents both integers and floating-point numbers.

TypeScript uses the same number type for all numeric values.

let decimal: number = 6;
let hex: number = 0xf00d; // Hexadecimal
let binary: number = 0b1010; // Binary
let octal: number = 0o744; // Octal
let float: number = 3.14; // Floating point
  1. String

Represents text data.

Can use single quotes (’), double quotes (”), or backticks (`) for template literals.

let color: string = "blue";
let fullName: string = 'John Doe';
let age: number = 30;
let sentence: string = `Hello, my name is ${fullName} and I'll be ${age + 1} next year.`;
  1. BigInt

Represents whole numbers larger than 253 - 1.

Use the n suffix to create a bigint.

const bigNumber: bigint = 9007199254740991n;
const hugeNumber = BigInt(9007199254740991); // Alternative syntax
  1. Symbol

Creates unique identifiers.

Useful for creating unique property keys and constants.

const uniqueKey: symbol = Symbol('description');
const obj = {
[uniqueKey]: 'This is a unique property'
};
console.log(obj[uniqueKey]); // "This is a unique property"

TypeScript Explicit Types and Inference

TypeScript offers two ways to work with types:

  1. Explicit Typing: You explicitly declare the type of a variable

Use explicit types for:

a. Function parameters and return types

b. Object literals

c. When the initial value might not be the final type

greeting: string = "Hello, TypeScript!";
// Number
userCount: number = 42;
// Boolean
isLoading: boolean = true;
// Array of numbers
scores: number[] = [100, 95, 98];
  1. Type Inference: TypeScript automatically determines the type based on the assigned value

Use type inference for:

a. Simple variable declarations with immediate assignment

b. When the type is obvious from the context

// TypeScript infers 'string'
let username = "alice";
// TypeScript infers 'number'
let score = 100;
// TypeScript infers 'boolean[]'
let flags = [true, false, true];
// TypeScript infers return type as 'number'
function add(a: number, b: number) {
return a + b;
}

TypeScript Special Types

TypeScript includes several special types that have specific behaviors in the type system.

These types are used in various scenarios to handle cases where the type might not be known in advance or when you need to work with JavaScript primitives in a type-safe way.

Type: any

The any type is the most flexible type in TypeScript.

It essentially tells the compiler to skip type checking for a particular variable.

While this can be useful in certain situations, it should be used sparingly as it bypasses TypeScript’s type safety features.

When to use any:

a. When migrating JavaScript code to TypeScript

b. When working with dynamic content where the type is unknown

c. When you need to opt out of type checking for a specific case

let v: any = true;
v = "string"; // no error as it can be "any" type
Math.round(v); // no error as it can be "any" type

Type: unknown

The unknown type is a type-safe counterpart of any.

It’s the type-safe way to say “this could be anything, so you must perform some type of checking before you use it”.

Key differences between unknown and any:

a. unknown must be type-checked before use

b. You can’t access properties on an unknown type without type assertion

c. You can’t call or construct values of type unknown

TypeScript will prevent unknown types from being used without proper type checking, as shown in the example below:

let w: unknown = 1;
w = "string"; // no error
or
function process(value: unknown) {
if (typeof value === 'string') {
return value.toUpperCase(); // Now safe
}
}

When to use unknown:

a. When working with data from external sources (APIs, user input, etc.)

b. When you want to ensure type safety while still allowing flexibility

c. When migrating from JavaScript to TypeScript in a type-safe way

Type: never

The never type represents the type of values that never occur.

It’s used to indicate that something never happens or should never happen.

Common use cases for never:

a. Functions that never return (always throw an error or enter an infinite loop)

b. Type guards that never pass type checking

c. Exhaustiveness checking in discriminated unions

Examples of never in action

  1. Function that never returns
function throwError(message: string): never {
throw new Error(message);
}
  1. Basic never type (throws error when assigned)
type Direction = "left" | "right";
function move(dir: Direction) {
switch (dir) {
case "left":
console.log("go left");
break;
case "right":
console.log("go right");
break;
default:
const impossible: never = dir;
}
}
// add something to Direction
type Direction = "left" | "right" | "staright";
Error : Type '"staright"' is not assignable to type 'never'.
'impossible' is declared but its value is never read.

When to use never:

a. For functions that will never return a value

b. In type guards that should never match

c. For exhaustive type checking in switch statements

d. In generic types to indicate certain cases are impossible

Type: undefined & null

In TypeScript, both undefined and null have their own types, just like string or number.

By default, these types can be assigned to any other type, but this can be changed with TypeScript’s strict null checks.

Key points about undefined and null:

a. undefined means a variable has been declared but not assigned a value

b. null is an explicit assignment that represents no value or no object

c. In TypeScript, both have their own types: undefined and null respectively

d. With strictNullChecks enabled, you must explicitly handle these types

let y: undefined = undefined;
let z: null = null;

TypeScript Arrays

TypeScript has a specific syntax for typing arrays.

const names: string[] = [];
names.push("Dylan"); // no error
// names.push(3); // Error: Argument of type 'number' is not assignable to parameter of type 'string'

Readonly The readonly keyword can prevent arrays from being changed.

const names: readonly string[] = ["Dylan"];
names.push("Jack"); // Error: Property 'push' does not exist on type 'readonly string[]'.
// try removing the readonly modifier and see if it works?

TypeScript Tuples

A tuple is a typed array with a pre-defined length and types for each index.

Tuples are great because they allow each element in the array to be a known type of value.

To define a tuple, specify the type of each element in the array:

// define our tuple
let ourTuple: [number, boolean, string];
// initialize correctly
ourTuple = [5, false, 'Coding God was here'];

As you can see we have a number, boolean and a string.

But what happens if we try to set them in the wrong order:

// define our tuple
let ourTuple: [number, boolean, string];
// initialized incorrectly which throws an error
ourTuple = [false, 'Coding God was mistaken', 5];

Even though we have a boolean, string, and number the order matters in our tuple and will throw an error.

  1. Readonly Tuple

A good practice is to make your tuple readonly.

Tuples only have strongly defined types for the initial values:

// define our tuple
let ourTuple: [number, boolean, string];
// initialize correctly
ourTuple = [5, false, 'Coding God was here'];
// We have no type safety in our tuple for indexes 3+
ourTuple.push('Something new and wrong');
console.log(ourTuple);
result:
[ 5, false, 'Coding God was here', 'Something new and wrong' ]
You can see the new value.

Tuples only have strongly defined types for the initial values:

// define our readonly tuple
const ourReadonlyTuple: readonly [number, boolean, string] = [5, true, 'The Real Coding God'];
// throws error as it is readonly.
ourReadonlyTuple.push('Coding God took a day off');
  1. Named Tuples

Named tuples allow us to provide context for our values at each index.

const graph: [x: number, y: number] = [55.2, 41.3];

Named tuples provide more context for what our index values represent.

  1. Destructuring Tuples

Since tuples are arrays we can also destructure them.

const graph: [number, number] = [55.2, 41.3];
const [x, y] = graph;

TypeScript Object Types

TypeScript has a specific syntax for typing objects.

const car: { type: string, model: string, year: number } = {
type: "Toyota",
model: "Corolla",
year: 2009
};
  1. Optional Properties
const car: { type: string, mileage?: number } = { // no error because mileage is optional
type: "Toyota"
};
car.mileage = 2000;
  1. Index Signatures

Index signatures can be used for objects without a defined list of properties.

const nameAgeMap: { [index: string]: number } = {};
nameAgeMap.Jack = 25; // no error
nameAgeMap.Mark = "Fifty"; // Error: Type 'string' is not assignable to type 'number'.

TypeScript Enums

An enum is a special “class” that represents a group of constants (unchangeable variables).

Enums come in two flavors string and numeric.

Let’s start with numeric.

  1. Numeric Enums - Default

By default, enums will initialize the first value to 0 and add 1 to each additional value:

enum CardinalDirections {
North,
East,
South,
West
}
let currentDirection = CardinalDirections.North;
// logs 0
console.log(currentDirection);
// throws error as 'North' is not a valid enum
currentDirection = 'North'; // Error: "North" is not assignable to type 'CardinalDirections'.
  1. Numeric Enums - Initialized

You can set the value of the first numeric enum and have it auto increment from that:

enum CardinalDirections {
North = 1,
East,
South,
West
}
// logs 1
console.log(CardinalDirections.North);
// logs 4
console.log(CardinalDirections.West);
  1. Numeric Enums - Fully Initialized

You can assign unique number values for each enum value.

Then the values will not be incremented automatically:

enum StatusCodes {
NotFound = 404,
Success = 200,
Accepted = 202,
BadRequest = 400
}
// logs 404
console.log(StatusCodes.NotFound);
// logs 200
console.log(StatusCodes.Success);
  1. String Enums

Enums can also contain strings.

This is more common than numeric enums, because of their readability and intent.

enum CardinalDirections {
North = 'North',
East = "East",
South = "South",
West = "West"
};
// logs "North"
console.log(CardinalDirections.North);
// logs "West"
console.log(CardinalDirections.West);

TypeScript Type Aliases and Interfaces

TypeScript allows types to be defined separately from the variables that use them.

Aliases and Interfaces allows types to be easily shared between different variables/objects.

  1. Type Aliases

Type Aliases allow defining types with a custom name (an Alias).

Type Aliases can be used for primitives like string or more complex types such as objects and arrays:

type CarYear = number
type CarType = string
type CarModel = string
type Car = {
year: CarYear,
type: CarType,
model: CarModel
}
const carYear: CarYear = 2001
const carType: CarType = "Toyota"
const carModel: CarModel = "Corolla"
const car: Car = {
year: carYear,
type: carType,
model: carModel
};
  1. Interfaces

Interfaces are similar to type aliases, except they only apply to object types.

interface Rectangle {
height: number,
width: number
}
const rectangle: Rectangle = {
height: 20,
width: 10
};

Type vs Interface: Key Differences

  1. Extending: Both can be extended, but interfaces support declaration merging.

  2. Unions/Intersections: Only type aliases support union and intersection types.

  3. Implements: Classes can implement either.

  4. Recommendation: Use interface for objects, type for everything else.

  5. Extending Interfaces

Interfaces can extend each other’s definition.

Extending an interface means you are creating a new interface with the same properties as the original, plus something new.

interface Rectangle {
height: number,
width: number
}
interface ColoredRectangle extends Rectangle {
color: string
}
const coloredRectangle: ColoredRectangle = {
height: 20,
width: 10,
color: "red"
};

TypeScript Union Types

Union types are used when a value can be more than a single type.

Such as when a property would be string or number.

  1. Union | (OR)

Using the | we are saying our parameter is a string or number:

function printStatusCode(code: string | number) {
console.log(`My status code is ${code}.`)
}
printStatusCode(404);
printStatusCode('404');
  1. Union Type Errors
function printStatusCode(code: string | number) {
console.log(`My status code is ${code.toUpperCase()}.`) // error: Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'
}

TypeScript Functions

TypeScript has a specific syntax for typing function parameters and return values.

  1. Return Type

The type of the value returned by the function can be explicitly defined.

// the `: number` here specifies that this function returns a number
function getTime(): number {
return new Date().getTime();
}
  1. Parameters

Function parameters are typed with a similar syntax as variable declarations.

function multiply(a: number, b: number) {
return a * b;
}
  1. Optional Parameters

By default TypeScript will assume all parameters are required, but they can be explicitly marked as optional.

// the `?` operator here marks parameter `c` as optional
function add(a: number, b: number, c?: number) {
return a + b + (c || 0);
}
  1. Default Parameters

For parameters with default values, the default value goes after the type annotation:

function pow(value: number, exponent: number = 10) {
return value ** exponent;
}
  1. Named Parameters

Typing named parameters follows the same pattern as typing normal parameters.

function divide({ dividend, divisor }: { dividend: number, divisor: number }) {
return dividend / divisor;
}
  1. Rest Parameters

Rest parameters can be typed like normal parameters, but the type must be an array as rest parameters are always arrays.

function add(a: number, b: number, ...rest: number[]) {
return a + b + rest.reduce((p, c) => p + c, 0);
}
  1. Type Alias

Function types can be specified separately from functions with type aliases.

These types are written similarly to arrow functions, read more about arrow functions here.

type Negate = (value: number) => number;
// in this function, the parameter `value` automatically gets assigned the type `number` from the type `Negate`
const negateFunction: Negate = (value) => value * -1;

TypeScript Casting

There are times when working with types where it’s necessary to override the type of a variable, such as when incorrect types are provided by a library.

Casting is the process of overriding a type.

  1. Casting with as

A straightforward way to cast a variable is using the as keyword, which will directly change the type of the given variable.

let x: unknown = 'hello';
console.log((x as string).length);
  1. Casting with <>

Using <> works the same as casting with as.

let x: unknown = 'hello';
console.log((<string>x).length);
  1. Force casting

To override type errors that TypeScript may throw when casting, first cast to unknown, then to the target type.

let x = 'hello';
console.log(((x as unknown) as number).length); // x is not actually a number so this will return undefined

TypeScript Classes

TypeScript adds types and visibility modifiers to JavaScript classes.

  1. Members: Types

The members of a class (properties & methods) are typed using type annotations, similar to variables.

class Person {
name: string;
}
const person = new Person();
person.name = "Jane";
  1. Members: Visibility

Class members can also be given special modifiers that affect visibility.

There are three main visibility modifiers in TypeScript.

a. public - (default) allows access to the class member from anywhere

b. private - only allows access to the class member from within the class

c. protected - allows access to the class member from itself and any classes that inherit it, which is covered in the inheritance section below

class Person {
private name: string;
public constructor(name: string) {
this.name = name;
}
public getName(): string {
return this.name;
}
}
const person = new Person("Jane");
console.log(person.getName()); // person.name isn't accessible from outside the class since it's private
  1. Parameter Properties

TypeScript provides a convenient way to define class members in the constructor, by adding a visibility modifier to the parameter.

class Person {
// name is a private member variable
public constructor(private name: string) {}
public getName(): string {
return this.name;
}
}
const person = new Person("Jane");
console.log(person.getName());
  1. Readonly

Similar to arrays, the readonly keyword can prevent class members from being changed.

class Person {
private readonly name: string;
public constructor(name: string) {
// name cannot be changed after this initial definition, which has to be either at its declaration or in the constructor.
this.name = name;
}
public getName(): string {
return this.name;
}
}
const person = new Person("Jane");
console.log(person.getName());
  1. Inheritance: Implements

Interfaces can be used to define the type a class must follow through the implements keyword.

interface Shape {
getArea: () => number;
}
class Rectangle implements Shape {
public constructor(protected readonly width: number, protected readonly height: number) {}
public getArea(): number {
return this.width * this.height;
}
}
  1. Inheritance: Extends

Classes can extend each other through the extends keyword.

interface Shape {
getArea: () => number;
}
class Rectangle implements Shape {
public constructor(protected readonly width: number, protected readonly height: number) {}
public getArea(): number {
return this.width * this.height;
}
}
class Square extends Rectangle {
public constructor(width: number) {
super(width, width);
}
// getArea gets inherited from Rectangle
}
  1. Override

When a class extends another class, it can replace the members of the parent class with the same name.

Newer versions of TypeScript allow explicitly marking this with the override keyword.

interface Shape {
getArea: () => number;
}
class Rectangle implements Shape {
// using protected for these members allows access from classes that extend from this class, such as Square
public constructor(protected readonly width: number, protected readonly height: number) {}
public getArea(): number {
return this.width * this.height;
}
public toString(): string {
return `Rectangle[width=${this.width}, height=${this.height}]`;
}
}
class Square extends Rectangle {
public constructor(width: number) {
super(width, width);
}
// this toString replaces the toString from Rectangle
public override toString(): string {
return `Square[width=${this.width}]`;
}
}
  1. Abstract Classes

Classes can be written in a way that allows them to be used as a base class for other classes without having to implement all the members.

This is done by using the abstract keyword.

Members that are left unimplemented also use the abstract keyword.

abstract class Polygon {
public abstract getArea(): number;
public toString(): string {
return `Polygon[area=${this.getArea()}]`;
}
}
class Rectangle extends Polygon {
public constructor(protected readonly width: number, protected readonly height: number) {
super();
}
public getArea(): number {
return this.width * this.height;
}
}

TypeScript Basic Generics

Generics allow creating ‘type variables’ which can be used to create classes, functions & type aliases that don’t need to explicitly define the types that they use.

Generics make it easier to write reusable code.

  1. Functions

Generics with functions help create more general functions that accurately represent the input and return types.

function createPair<S, T>(v1: S, v2: T): [S, T] {
return [v1, v2];
}
console.log(createPair<string, number>('hello', 42)); // ['hello', 42]
  1. Classes

Generics can be used to create generalized classes, like Map.

class NamedValue<T> {
private _value: T | undefined;
constructor(private name: string) {}
public setValue(value: T) {
this._value = value;
}
public getValue(): T | undefined {
return this._value;
}
public toString(): string {
return `${this.name}: ${this._value}`;
}
}
let value = new NamedValue<number>('myNumber');
value.setValue(10);
console.log(value.toString()); // myNumber: 10
  1. Type Aliases

Generics in type aliases allow creating types that are more reusable.

type Wrapped<T> = { value: T };
const wrappedValue: Wrapped<number> = { value: 10 };
  1. Default Value

Generics can be assigned default values which apply if no other value is specified or inferred.

class NamedValue<T = string> {
private _value: T | undefined;
constructor(private name: string) {}
public setValue(value: T) {
this._value = value;
}
public getValue(): T | undefined {
return this._value;
}
public toString(): string {
return `${this.name}: ${this._value}`;
}
}
let value = new NamedValue('myNumber');
value.setValue('myValue');
console.log(value.toString()); // myNumber: myValue
  1. Extends

Constraints can be added to generics to limit what’s allowed.

The constraints make it possible to rely on a more specific type when using the generic type.

function createLoggedPair<S extends string | number, T extends string | number>(v1: S, v2: T): [S, T] {
console.log(`creating pair: v1='${v1}', v2='${v2}'`);
return [v1, v2];
}

TypeScript Utility Types

TypeScript comes with a large number of types that can help with some common type manipulation, usually referred to as utility types.

This chapter covers the most popular utility types.

  1. Partial

Partial changes all the properties in an object to be optional.

interface Point {
x: number;
y: number;
}
let pointPart: Partial<Point> = {}; // `Partial` allows x and y to be optional
pointPart.x = 10;
  1. Required

Required changes all the properties in an object to be required.

interface Car {
make: string;
model: string;
mileage?: number;
}
let myCar: Required<Car> = {
make: 'Ford',
model: 'Focus',
mileage: 12000 // `Required` forces mileage to be defined
};
  1. Record

Record is a shortcut to defining an object type with a specific key type and value type.

const nameAgeMap: Record<string, number> = {
'Alice': 21,
'Bob': 25
};

Record<string, number> is equivalent to { [key: string]: number }

  1. Omit

Omit removes keys from an object type.

interface Person {
name: string;
age: number;
location?: string;
}
const bob: Omit<Person, 'age' | 'location'> = {
name: 'Bob'
// `Omit` has removed age and location from the type and they can't be defined here
};
  1. Pick

Pick removes all but the specified keys from an object type.

interface Person {
name: string;
age: number;
location?: string;
}
const bob: Pick<Person, 'name'> = {
name: 'Bob'
// `Pick` has only kept name, so age and location were removed from the type and they can't be defined here
};
  1. Exclude

Exclude removes types from a union.

type Primitive = string | number | boolean
const value: Exclude<Primitive, string> = true; // a string cannot be used here since Exclude removed it from the type.
  1. ReturnType

ReturnType extracts the return type of a function type.

type PointGenerator = () => { x: number; y: number; };
const point: ReturnType<PointGenerator> = {
x: 10,
y: 20
};
  1. Parameters

Parameters extracts the parameter types of a function type as an array.

type PointPrinter = (p: { x: number; y: number; }) => void;
const point: Parameters<PointPrinter>[0] = {
x: 10,
y: 20
};
  1. Readonly

Readonly is used to create a new type where all properties are readonly, meaning they cannot be modified once assigned a value.

interface Person {
name: string;
age: number;
}
const person: Readonly<Person> = {
name: "Dylan",
age: 35,
};
person.name = 'Israel'; // prog.ts(11,8): error TS2540: Cannot assign to 'name' because it is a read-only property.

TypeScript Keyof

keyof is a keyword in TypeScript which is used to extract the key type from an object type.

  1. keyof with explicit keys

When used on an object type with explicit keys, keyof creates a union type with those keys.

interface Person {
name: string;
age: number;
}
// `keyof Person` here creates a union type of "name" and "age", other strings will not be allowed
function printPersonProperty(person: Person, property: keyof Person) {
console.log(`Printing person property ${property}: "${person[property]}"`);
}
let person = {
name: "Max",
age: 27
};
printPersonProperty(person, "name"); // Printing person property name: "Max"
  1. keyof with index signatures

keyof can also be used with index signatures to extract the index type.

type StringMap = { [key: string]: unknown };
// `keyof StringMap` resolves to `string` here
function createStringPair(property: keyof StringMap, value: string): StringMap {
return { [property]: value };
}

TypeScript with Node.js

Q. Why Use TypeScript with Node.js?

TypeScript brings static typing to Node.js development, providing better tooling, improved code quality, and enhanced developer experience.

Key benefits include:

a. Type safety for JavaScript code

b. Better IDE support with autocompletion

c. Early error detection during development

d. Improved code maintainability and documentation

e. Easier refactoring

Setting Up a TypeScript Node.js Project

  1. Initialize a New Project
mkdir my-ts-node-app
cd my-ts-node-app
npm init -y
npm install typescript @types/node --save-dev
npx tsc --init
  1. Create a Source Folder

Keep source code in src/ and compiled output in dist/.

mkdir src
  1. Configure TypeScript

Edit the generated tsconfig.json:

{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

Option highlights:

rootDir/outDir: keeps source (src) separate from build output (dist).

strict: enables the safest type checking.

esModuleInterop: smoother interop with CommonJS/ES modules.

sourceMap: generate maps for debugging compiled code.

  1. Install Runtime and Dev Dependencies

Install Express for HTTP handling and helpful dev tools:

npm install express body-parser
npm install --save-dev ts-node nodemon @types/express

Warning: Use ts-node and nodemon only for development.

For production, compile with tsc and run Node on the JS output.

  1. Project Structure
my-ts-node-app/
src/
server.ts
middleware/
auth.ts
entity/
User.ts
config/
database.ts
dist/
node_modules/
package.json
tsconfig.json

TypeScript Type Guards

TypeScript Type Guards are powerful constructs that allow you to narrow down the type of a variable within a specific scope.

They help TypeScript understand and enforce type safety by providing explicit checks that determine the specific type of a variable at runtime.

  1. typeof Type Guards

The typeof operator is a built-in type guard that checks the type of a primitive value at runtime.

It’s particularly useful for narrowing primitive types like strings, numbers, booleans, etc.

// Simple type guard with typeof
function formatValue(value: string | number): string {
if (typeof value === 'string') {
// TypeScript knows value is string here
return value.trim().toUpperCase();
} else {
// TypeScript knows value is number here
return value.toFixed(2);
}
}
// Example usage
const result1 = formatValue(' hello '); // "HELLO"
const result2 = formatValue(42.1234); // "42.12"
  1. instanceof Type Guards

The instanceof operator checks if an object is an instance of a specific class or constructor function.

It’s useful for narrowing types with custom classes or built-in objects.

class Bird {
fly() {
console.log("Flying...");
}
}
class Fish {
swim() {
console.log("Swimming...");
}
}
function move(animal: Bird | Fish) {
if (animal instanceof Bird) {
// TypeScript knows animal is Bird here
animal.fly();
} else {
// TypeScript knows animal is Fish here
animal.swim();
}
}
  1. User-Defined Type Guards

For more complex type checking, you can create custom type guard functions using type predicates.

These are functions that return a type predicate in the form parameterName is Type.

interface Car {
make: string;
model: string;
year: number;
}
interface Motorcycle {
make: string;
model: string;
year: number;
type: "sport" | "cruiser";
}
// Type predicate function
function isCar(vehicle: Car | Motorcycle): vehicle is Car {
return (vehicle as Motorcycle).type === undefined;
}
function displayVehicleInfo(vehicle: Car | Motorcycle) {
console.log(`Make: ${vehicle.make}, Model: ${vehicle.model}, Year: ${vehicle.year}`);
if (isCar(vehicle)) {
// TypeScript knows vehicle is Car here
console.log("This is a car");
} else {
// TypeScript knows vehicle is Motorcycle here
console.log(`This is a ${vehicle.type} motorcycle`);
}
}
  1. Discriminated Unions

Discriminated unions (also known as tagged unions) use a common property (the discriminant) to distinguish between different object types in a union.

This pattern is particularly powerful when combined with type guards.

interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function calculateArea(shape: Shape) {
switch (shape.kind) {
case "circle":
// TypeScript knows shape is Circle here
return Math.PI * shape.radius ** 2;
case "square":
// TypeScript knows shape is Square here
return shape.sideLength ** 2;
}
}
  1. The in Operator

The in operator checks for the existence of a property on an object.

It’s particularly useful for narrowing union types where different types have distinct properties.

interface Dog {
bark(): void;
}
interface Cat {
meow(): void;
}
function makeSound(animal: Dog | Cat) {
if ("bark" in animal) {
// TypeScript knows animal is Dog here
animal.bark();
} else {
// TypeScript knows animal is Cat here
animal.meow();
}
}

TypeScript Literal Types

Literal types in TypeScript allow you to specify exact values that variables can hold, providing more precision than broader types like string or number.

They are the building blocks for creating precise and type-safe applications.

  1. String Literal Types

A string literal type represents a specific string value:

// A variable with a string literal type
let direction: "north" | "south" | "east" | "west";
// Valid assignments
direction = "north";
direction = "south";
// Invalid assignments would cause errors
// direction = "northeast"; // Error: Type '"northeast"' is not assignable to type '"north" | "south" | "east" | "west"'
// direction = "up"; // Error: Type '"up"' is not assignable to type '"north" | "south" | "east" | "west"'
// Using string literal types in functions
function move(direction: "north" | "south" | "east" | "west") {
console.log(`Moving ${direction}`);
}
move("east"); // Valid
// move("up"); // Error: Argument of type '"up"' is not assignable to parameter of type...
  1. Numeric Literal Types
// A variable with a numeric literal type
let diceRoll: 1 | 2 | 3 | 4 | 5 | 6;
// Valid assignments
diceRoll = 1;
diceRoll = 6;
// Invalid assignments would cause errors
// diceRoll = 0; // Error: Type '0' is not assignable to type '1 | 2 | 3 | 4 | 5 | 6'
// diceRoll = 7; // Error: Type '7' is not assignable to type '1 | 2 | 3 | 4 | 5 | 6'
// diceRoll = 2.5; // Error: Type '2.5' is not assignable to type '1 | 2 | 3 | 4 | 5 | 6'
// Using numeric literal types in functions
function rollDice(): 1 | 2 | 3 | 4 | 5 | 6 {
return Math.floor(Math.random() * 6) + 1 as 1 | 2 | 3 | 4 | 5 | 6;
}
const result = rollDice();
console.log(`You rolled a ${result}`);
  1. Boolean Literal Types

Boolean literal types are less commonly used since there are only two boolean values, but they can be useful in specific scenarios:

// A type that can only be the literal value 'true'
type YesOnly = true;
// A function that must return true
function alwaysSucceed(): true {
// Always returns the literal value 'true'
return true;
}
// Boolean literal combined with other types
type SuccessFlag = true | "success" | 1;
type FailureFlag = false | "failure" | 0;
function processResult(result: SuccessFlag | FailureFlag) {
if (result === true || result === "success" || result === 1) {
console.log("Operation succeeded");
} else {
console.log("Operation failed");
}
}
processResult(true); // "Operation succeeded"
processResult("success"); // "Operation succeeded"
processResult(1); // "Operation succeeded"
processResult(false); // "Operation failed"
  1. Literal Types with Objects

Literal types can be combined with object types to create very specific shapes:

// Object with literal property values
type HTTPSuccess = {
status: 200 | 201 | 204;
statusText: "OK" | "Created" | "No Content";
data: any;
};
type HTTPError = {
status: 400 | 401 | 403 | 404 | 500;
statusText: "Bad Request" | "Unauthorized" | "Forbidden" | "Not Found" | "Internal Server Error";
error: string;
};
type HTTPResponse = HTTPSuccess | HTTPError;
function handleResponse(response: HTTPResponse) {
if (response.status >= 200 && response.status < 300) {
console.log(`Success: ${response.statusText}`);
console.log(response.data);
} else {
console.log(`Error ${response.status}: ${response.statusText}`);
console.log(`Message: ${response.error}`);
}
}
// Example usage
const successResponse: HTTPSuccess = {
status: 200,
statusText: "OK",
data: { username: "john_doe", email: "john@example.com" }
};
const errorResponse: HTTPError = {
status: 404,
statusText: "Not Found",
error: "User not found in database"
};
handleResponse(successResponse);
handleResponse(errorResponse);

TypeScript Async Programming

TypeScript enhances JavaScript’s asynchronous capabilities with static typing, making your async code more predictable and maintainable.

This guide covers everything from basic async/await to advanced patterns.

  1. Promises in TypeScript

TypeScript enhances JavaScript Promises with type safety through generics.

A Promise represents an asynchronous operation that will complete with a value of type T or fail with a reason of type any.

key Points:

a. Promise - Generic type where T is the type of the resolved value

b. Promise - For Promises that don’t return a value

c. Promise - For Promises that never resolve (rare)

// Create a typed Promise that resolves to a string
const fetchGreeting = (): Promise<string> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
resolve("Hello, TypeScript!");
} else {
reject(new Error("Failed to fetch greeting"));
}
}, 1000);
});
};
// Using the Promise with proper type inference
fetchGreeting()
.then((greeting) => {
// TypeScript knows 'greeting' is a string
console.log(greeting.toUpperCase());
})
.catch((error: Error) => {
console.error("Error:", error.message);
});
  1. Async/Await with TypeScript

TypeScript’s async/await syntax provides a cleaner way to work with Promises, making asynchronous code look and behave more like synchronous code while maintaining type safety.

// Define types for our API response
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
// Function that returns a Promise of User array
async function fetchUsers(): Promise<User[]> {
console.log('Fetching users...');
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
return [
{ id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' },
{ id: 2, name: 'Bob', email: 'bob@example.com', role: 'user' }
];
}
// Async function to process users
async function processUsers() {
try {
// TypeScript knows users is User[]
const users = await fetchUsers();
console.log(`Fetched ${users.length} users`);
// Type-safe property access
const adminEmails = users
.filter(user => user.role === 'admin')
.map(user => user.email);
console.log('Admin emails:', adminEmails);
return users;
} catch (error) {
if (error instanceof Error) {
console.error('Failed to process users:', error.message);
} else {
console.error('An unknown error occurred');
}
throw error; // Re-throw to let caller handle
}
}
// Execute the async function
processUsers()
.then(users => console.log('Processing complete'))
.catch(err => console.error('Processing failed:', err));
  1. Promise Combinations

TypeScript provides powerful utility types and methods for working with multiple Promises.

These methods help you manage concurrent operations and handle their results in a type-safe way.

a. Promise.all - Parallel Execution

Run multiple promises in parallel and wait for all to complete.

Fails fast if any promise rejects.

// Different types of promises
const fetchUser = (id: number): Promise<{ id: number; name: string }> =>
Promise.resolve({ id, name: `User ${id}` });
const fetchPosts = (userId: number): Promise<Array<{ id: number; title: string }>> =>
Promise.resolve([ { id: 1, title: 'Post 1' },
{ id: 2, title: 'Post 2' }
]);
const fetchStats = (userId: number): Promise<{ views: number; likes: number }> =>
Promise.resolve({ views: 100, likes: 25 });
// Run all in parallel
async function loadUserDashboard(userId: number) {
try {
const [user, posts, stats] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchStats(userId)
]);
// TypeScript knows the types of user, posts, and stats
console.log(`User: ${user.name}`);
console.log(`Posts: ${posts.length}`);
console.log(`Likes: ${stats.likes}`);
return { user, posts, stats };
} catch (error) {
console.error('Failed to load dashboard:', error);
throw error;
}
}
// Execute with a user ID
loadUserDashboard(1);

b. Promise.race - First to Settle

Useful for timeouts or getting the first successful response from multiple sources.

/ Helper function for timeout
const timeout = (ms: number): Promise<never> =>
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
);
// Simulate API call with timeout
async function fetchWithTimeout<T>(
promise: Promise<T>,
timeoutMs: number = 5000
): Promise<T> {
return Promise.race([
promise,
timeout(timeoutMs).then(() => {
throw new Error(`Request timed out after ${timeoutMs}ms`);
}),
]);
}
// Usage example
async function fetchUserData() {
try {
const response = await fetchWithTimeout(
fetch('https://api.example.com/user/1'),
3000 // 3 second timeout
);
const data = await response.json();
return data;
} catch (error) {
console.error('Error:', (error as Error).message);
throw error;
}
}

c. Promise.allSettled - Handle All Results

When you want to wait for all promises to complete, regardless of success or failure.

// Simulate multiple API calls with different outcomes
const fetchData = async (id: number) => {
// Randomly fail some requests
if (Math.random() > 0.7) {
throw new Error(`Failed to fetch data for ID ${id}`);
}
return { id, data: `Data for ${id}` };
};
// Process multiple items with individual error handling
async function processBatch(ids: number[]) {
const promises = ids.map(id =>
fetchData(id)
.then(value => ({ status: 'fulfilled' as const, value }))
.catch(reason => ({ status: 'rejected' as const, reason }))
);
// Wait for all to complete
const results = await Promise.allSettled(promises);
// Process results
const successful = results
.filter((result): result is PromiseFulfilledResult<{ status: 'fulfilled', value: any }> =>
result.status === 'fulfilled' &&
result.value.status === 'fulfilled'
)
.map(r => r.value.value);
const failed = results
.filter((result): result is PromiseRejectedResult |
PromiseFulfilledResult<{ status: 'rejected', reason: any }> => {
if (result.status === 'rejected') return true;
return result.value.status === 'rejected';
});
console.log(`Successfully processed: ${successful.length}`);
console.log(`Failed: ${failed.length}`);
return { successful, failed };
}
// Process a batch of IDs
processBatch([1, 2, 3, 4, 5]);

TypeScript in JavaScript Projects

To enable TypeScript checking in JavaScript files, you need to:

a. Create a tsconfig.json file (if you don’t have one)

b. Enable checkJs or use // @ts-check in individual files

// @ts-check
/**
* Adds two numbers.
* @param {number} a
* @param {number} b
* @returns {number}
*/
function add(a, b) {
return a + b;
}