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:
- Boolean
Represents true/false values.
Used for flags, toggles, and conditions.
let isActive: boolean = true;let hasPermission = false; // TypeScript infers 'boolean' type- 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; // Hexadecimallet binary: number = 0b1010; // Binarylet octal: number = 0o744; // Octallet float: number = 3.14; // Floating point- 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.`;- 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- 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:
- 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!";
// NumberuserCount: number = 42;
// BooleanisLoading: boolean = true;
// Array of numbersscores: number[] = [100, 95, 98];- 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" typeType: 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
- Function that never returns
function throwError(message: string): never { throw new Error(message);}- 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 Directiontype 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 tuplelet ourTuple: [number, boolean, string];
// initialize correctlyourTuple = [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 tuplelet ourTuple: [number, boolean, string];
// initialized incorrectly which throws an errorourTuple = [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.
- Readonly Tuple
A good practice is to make your tuple readonly.
Tuples only have strongly defined types for the initial values:
// define our tuplelet ourTuple: [number, boolean, string];// initialize correctlyourTuple = [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 tupleconst 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');- 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.
- 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};- Optional Properties
const car: { type: string, mileage?: number } = { // no error because mileage is optional type: "Toyota"};car.mileage = 2000;- Index Signatures
Index signatures can be used for objects without a defined list of properties.
const nameAgeMap: { [index: string]: number } = {};nameAgeMap.Jack = 25; // no errornameAgeMap.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.
- 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 0console.log(currentDirection);// throws error as 'North' is not a valid enumcurrentDirection = 'North'; // Error: "North" is not assignable to type 'CardinalDirections'.- 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 1console.log(CardinalDirections.North);// logs 4console.log(CardinalDirections.West);- 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 404console.log(StatusCodes.NotFound);// logs 200console.log(StatusCodes.Success);- 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.
- 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 = numbertype CarType = stringtype CarModel = stringtype Car = { year: CarYear, type: CarType, model: CarModel}
const carYear: CarYear = 2001const carType: CarType = "Toyota"const carModel: CarModel = "Corolla"const car: Car = { year: carYear, type: carType, model: carModel};- 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
-
Extending: Both can be extended, but interfaces support declaration merging.
-
Unions/Intersections: Only type aliases support union and intersection types.
-
Implements: Classes can implement either.
-
Recommendation: Use interface for objects, type for everything else.
-
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.
- 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');- 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.
- Return Type
The type of the value returned by the function can be explicitly defined.
// the `: number` here specifies that this function returns a numberfunction getTime(): number { return new Date().getTime();}- Parameters
Function parameters are typed with a similar syntax as variable declarations.
function multiply(a: number, b: number) { return a * b;}- 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 optionalfunction add(a: number, b: number, c?: number) { return a + b + (c || 0);}- 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;}- Named Parameters
Typing named parameters follows the same pattern as typing normal parameters.
function divide({ dividend, divisor }: { dividend: number, divisor: number }) { return dividend / divisor;}- 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);}- 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.
- 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);- Casting with <>
Using <> works the same as casting with as.
let x: unknown = 'hello';console.log((<string>x).length);- 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 undefinedTypeScript Classes
TypeScript adds types and visibility modifiers to JavaScript classes.
- 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";- 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- 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());- 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());- 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; }}- 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}- 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}]`; }}- 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.
- 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]- 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- Type Aliases
Generics in type aliases allow creating types that are more reusable.
type Wrapped<T> = { value: T };
const wrappedValue: Wrapped<number> = { value: 10 };- 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- 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.
- 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 optionalpointPart.x = 10;- 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};- 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 }
- 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};- 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};- Exclude
Exclude removes types from a union.
type Primitive = string | number | booleanconst value: Exclude<Primitive, string> = true; // a string cannot be used here since Exclude removed it from the type.- ReturnType
ReturnType extracts the return type of a function type.
type PointGenerator = () => { x: number; y: number; };const point: ReturnType<PointGenerator> = { x: 10, y: 20};- 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};- 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.
- 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 allowedfunction 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"- 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` herefunction 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
- Initialize a New Project
mkdir my-ts-node-appcd my-ts-node-appnpm init -ynpm install typescript @types/node --save-devnpx tsc --init- Create a Source Folder
Keep source code in src/ and compiled output in dist/.
mkdir src- 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.
- Install Runtime and Dev Dependencies
Install Express for HTTP handling and helpful dev tools:
npm install express body-parsernpm install --save-dev ts-node nodemon @types/expressWarning: Use ts-node and nodemon only for development.
For production, compile with tsc and run Node on the JS output.
- Project Structure
my-ts-node-app/ src/ server.ts middleware/ auth.ts entity/ User.ts config/ database.ts dist/ node_modules/ package.json tsconfig.jsonTypeScript 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.
- 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 typeoffunction 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 usageconst result1 = formatValue(' hello '); // "HELLO"const result2 = formatValue(42.1234); // "42.12"- 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(); }}- 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 functionfunction 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`); }}- 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; }}- 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.
- String Literal Types
A string literal type represents a specific string value:
// A variable with a string literal typelet direction: "north" | "south" | "east" | "west";
// Valid assignmentsdirection = "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 functionsfunction 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...- Numeric Literal Types
// A variable with a numeric literal typelet diceRoll: 1 | 2 | 3 | 4 | 5 | 6;
// Valid assignmentsdiceRoll = 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 functionsfunction 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}`);- 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 truefunction alwaysSucceed(): true { // Always returns the literal value 'true' return true;}
// Boolean literal combined with other typestype 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"- Literal Types with Objects
Literal types can be combined with object types to create very specific shapes:
// Object with literal property valuestype 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 usageconst 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.
- Promises in TypeScript
TypeScript enhances JavaScript Promises with type safety through generics.
A Promise
key Points:
a. Promise
b. Promise
c. Promise
// Create a typed Promise that resolves to a stringconst 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 inferencefetchGreeting() .then((greeting) => { // TypeScript knows 'greeting' is a string console.log(greeting.toUpperCase()); }) .catch((error: Error) => { console.error("Error:", error.message); });- 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 responseinterface User { id: number; name: string; email: string; role: 'admin' | 'user' | 'guest';}
// Function that returns a Promise of User arrayasync 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 usersasync 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 functionprocessUsers() .then(users => console.log('Processing complete')) .catch(err => console.error('Processing failed:', err));- 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 promisesconst 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 parallelasync 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 IDloadUserDashboard(1);b. Promise.race - First to Settle
Useful for timeouts or getting the first successful response from multiple sources.
/ Helper function for timeoutconst timeout = (ms: number): Promise<never> => new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms) );
// Simulate API call with timeoutasync 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 exampleasync 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 outcomesconst 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 handlingasync 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 IDsprocessBatch([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;}