Skip to main content

Types

Type Annotations

In TypeScript, a type annotation tells the compiler what type of value a variable, parameter, or function will hold. It improves type safety by catching errors during compilation rather than at runtime.

let variableName: type = value;
CategoryType / FeatureExampleUse Case
Primitivesstringlet name: string = "Masum";Text data
numberlet age: number = 25;Numbers (int/float)
booleanlet isActive: boolean = true;True/false
bigintlet big: bigint = 123n;Arbitrary large integers
symbollet id: symbol = Symbol("id");Unique IDs
nulllet empty: null = null;Explicit empty value
undefinedlet notDef: undefined = undefined;Variable not initialized
Specialanylet anything: any = "hi";Opt-out of type checking (unsafe)
unknownlet value: unknown = "hi";Safer alternative to any
neverfunction fail(): never { throw new Error("x"); }Functions that never return
voidfunction log(msg: string): void {}No return value
Objectsobjectlet obj: object = { x: 10 };Non-primitive types
Interfaceinterface User { id: number; name: string }Define object shape
Classclass Person { constructor(public n: string) {} }OOP style
Type aliastype Point = { x: number; y: number };Custom named type
CollectionsArraylet nums: number[] = [1,2,3];Ordered lists
Tuplelet p: [string, number] = ["Alice", 30];Fixed-length array
ReadonlyArraylet arr: ReadonlyArray<number> = [1,2];Immutable arrays
Combinations**Union (``)**let id: string | number;Multiple possible types
Intersection (&)type P = A & B;Combine multiple types
Literal typeslet dir: "north" | "south";Restrict to exact values
Enums & GenericsEnumenum Role { Admin, User }Named constants
Genericsfunction id<T>(x: T): T { return x; }Reusable, type-safe
Utility TypesPartial<T>Partial<User>Make all fields optional
Required<T>Required<User>Make all fields required
Readonly<T>Readonly<User>Make fields immutable
Pick<T,K>Pick<User,"id">Pick subset of fields
Omit<T,K>Omit<User,"age">Exclude certain fields
Record<K,V>Record<string, number>Map keys to values
FunctionsFunction typelet add: (a:number,b:number)=>number;Explicit function typing
Constructor typetype C<T> = new (...a:any[]) => T;Class constructors

unknown

The unknown type is similar to any, but safer. You can assign anything to unknown, but before using it, you must check its type.

let input: unknown;

input = "Hello";
input = 42;

// TypeScript requires a type check before using
if (typeof input === "string") {
console.log(input.toUpperCase()); // Safe ✅
}

// Directly calling input.toUpperCase() would be an error ❌

With unknown, TypeScript forces you to narrow down the type before using it.

Type Inferences

In TypeScript, Type Inference means that the compiler can automatically determine the type of a variable, function return, or expression even if you don’t explicitly specify it.

Why is Type Inference Important?

  • Reduces boilerplate → You don’t need to annotate everything with types.
  • Keeps code clean → Less clutter from explicit types everywhere.
  • Maintains type safety → Even without annotations, TypeScript enforces correct usage.

Basic Example

let message = "Hello, TypeScript!";
  • Here, you didn’t write let message: string.
  • TypeScript infers that message is a string because the initial value is a string.
  • So now:
message = "Hi!"; // ✅ Allowed
message = 42; // ❌ Error: Type 'number' is not assignable to type 'string'

Common Scenarios of Type Inference

  1. Variable Initialization

    If you assign a value at declaration, TypeScript infers the type.

    let age = 25; // inferred as number
    let isActive = true; // inferred as boolean
  2. Function Return Type

    If you don’t explicitly declare the return type, TypeScript infers it.

    function add(a: number, b: number) {
    return a + b;
    }
    // inferred return type: number

    You didn’t write : number after the function, but TypeScript knows the result of a + b is a number.

  3. Array Inference

    TypeScript infers array types from initial elements.

    let numbers = [1, 2, 3];
    // inferred as number[]

    numbers.push(4); // ✅ Allowed
    numbers.push("hi"); // ❌ Error: string not assignable to number

    If you mix types:

    let mixed = [1, "hello", true];
    // inferred as (string | number | boolean)[]
  4. Contextual Typing

    Sometimes inference works from context.

    window.addEventListener("click", (event) => {
    console.log(event.clientX); // event is inferred as MouseEvent
    });

    Here, you didn’t type event: MouseEvent. TypeScript inferred it because "click" handlers receive a MouseEvent.

  5. Best Common Type

    When multiple types are possible, TypeScript finds a common type.

    let values = [1, 2, null];
    // inferred as (number | null)[]

Type Widening

  • If you don’t give a type or initial value, TypeScript infers message: any.
  • If you don't give a type, Typescripts infers the type as values type.

When you declare a variable without assignment:

let data;
// inferred as 'any'
data = 42; // allowed
data = "hi"; // allowed

If you assign a literal:

let status = "loading";
// inferred as string (not the literal "loading")

But if you want a literal type:

const status = "loading";
// inferred as "loading" (literal type)

Object Literals Losing Strictness

let user = { name: "Alice", age: 25 };
user.location = "USA"; // ❌ Error: Property 'location' does not exist

But if you use any inference:

let user: any = {};
user.name = "Alice"; // ✅
user.age = 25; // ✅
user.location = "USA"; // ✅ (but unsafe!)
  • TypeScript loses track of structure.

null and undefined Issues

let value = null;
// inferred type: any
value = 123; // ✅ allowed
value = "hello"; // ✅ allowed

This can cause confusion because value doesn’t stay consistent.

let value: number | null = null;
value = 123; // ✅
value = "hi"; // ❌ Error

Union Types

A Union Type allows a value to be one of several possible types. It’s written with the | (pipe) symbol.

let value: string | number;

Here, value can be either a string or a number.

Type Narrowing with Union

When you use a union, you may need type checks to access type-specific properties.

function formatId(id: string | number) {
if (typeof id === "string") {
return id.toUpperCase(); // ✅ Works only for string
}
return id.toFixed(2); // ✅ Works only for number
}

Intersection Types

An Intersection Type combines multiple types into one single type that must include all members of those types. It’s written with the & (ampersand) symbol.

type A = { name: string };
type B = { age: number };

type Person = A & B;

const user: Person = {
name: "Alice",
age: 30,
};

Here, Person must have both name and age.

Intersection with Primitives

type Impossible = string & number;
  • This type is never possible (a value can’t be both string and number).
  • So Impossible becomes never (an impossible type).

Union vs Intersection

FeatureUnion (|) – ORIntersection (&) – AND
MeaningValue can be one of many typesValue must satisfy all types
UsageFlexibility (e.g., id: string | number)Combination (e.g., User & Admin)
Symbol| (pipe)& (ampersand)

Readonly properties

readonly with Arrays

TypeScript also supports ReadonlyArray<T>.

const numbers: ReadonlyArray<number> = [1, 2, 3];

numbers[0] = 10; // ❌ Error
numbers.push(4); // ❌ Error
  • ReadonlyArray<T> means you cannot modify the array’s contents.
  • You can still read from it.
  • If you want a mutable array → use number[].
  • If you want an immutable array → use ReadonlyArray<number>.

readonly vs const

Many beginners confuse readonly and const. They are different:

  • const: prevents reassignment of the variable binding (not the object properties).
  • readonly: prevents reassignment of the property inside an object or interface.
const obj = {
id: 1,
name: "Alice",
};

obj.name = "Bob"; // ✅ allowed (const doesn't protect properties)

type Person = {
readonly id: number;
};

const person: Person = { id: 1 };
person.id = 2; // ❌ Error (readonly protects property)

Readonly<T> Utility Type

TypeScript has a built-in Readonly<T> utility type that makes all properties of an object immutable.

interface Student {
id: number;
name: string;
}

const s: Readonly<Student> = {
id: 1,
name: "Masum",
};

s.id = 2; // ❌ Error
s.name = "Ali"; // ❌ Error

readonly at Declaration

You can assign a default value directly.

class Config {
readonly appName: string = "MyApp"; // initialized at declaration
readonly version: number;

constructor(version: number) {
this.version = version; // ✅ allowed
}
}

const config = new Config(1.0);

console.log(config.appName); // MyApp
console.log(config.version); // 1
// config.appName = "OtherApp"; // ❌ Error

Type Aliases

A Type Alias lets you create a new name (alias) for a type. It doesn’t create a new type — it just gives a custom label to an existing type, which makes code more readable, reusable, and maintainable.

You define a type alias with the type keyword.

type AliasName = TypeDefinition;

Example

type UserId = string | number;

let id1: UserId = "abc123"; // ✅ allowed
let id2: UserId = 42; // ✅ allowed
let id3: UserId = true; // ❌ Error (boolean not part of UserId)
  • UserId is just an alias for string | number.
  • This avoids repeating string | number everywhere.

Recursive Type Alias

type Category = {
name: string;
subCategories?: Category[]; // recursive alias
};

const category: Category = {
name: "Electronics",
subCategories: [{ name: "Phones" }, { name: "Laptops" }],
};

Type Aliases cannot be reopened or changed later (unlike interfaces).

Differences between Type Aliases and Interfaces

  1. Definition
  • A type gives a new name to any type (primitive, union, intersection, object, etc.).
  • An interface is specifically used to describe the shape of an object (or function, class, etc.).
  1. Inheritance
  • Interface can be extended using extends:
interface Point {
x: number;
y: number;
}

interface Point3D extends Point {
z: number;
}

const point: Point3D = { x: 1, y: 2, z: 3 };
  • Type aliases can use intersections to achieve similar results:
type Point = { x: number; y: number };
type Point3D = Point & { z: number };

const point: Point3D = { x: 1, y: 2, z: 3 };
  1. Declaration Merging
  • Interfaces can merge if you declare them multiple times with the same name:
interface User {
name: string;
}

interface User {
age: number;
}

const user: User = { name: "Masum", age: 22 }; // ✅ Works
  • Type aliases cannot merge. Declaring the same type twice causes an error:
type User = { name: string };
type User = { age: number }; // ❌ Error: Duplicate identifier 'User'
  1. Other Type Features
  • Type aliases can represent:
    • Union types type ID = string | number;
    • Tuples type PointTuple = [number, number];
    • Primitive type type Name = string;
  • Interfaces cannot represent unions, primitives, or tuples. They are only for object shapes, function types, or classes.
  1. When to use which?
  • Use interface:
    • When you expect to extend or implement it.
    • When you want declaration merging.
    • Mostly for object-oriented programming patterns.
  • Use type:
    • When you need unions, intersections, tuples, or primitives.
    • For more flexible type compositions.

Interface vs Type Alias

FeatureInterfaceType Alias
Object shape✅ Yes✅ Yes
Extend / Inherit✅ Yes (extends)✅ Yes (&)
Declaration merging✅ Yes❌ No
Union / Intersection❌ No✅ Yes
Primitive / Tuple alias❌ No✅ Yes
FlexibilityLess flexibleMore flexible