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;
| Category | Type / Feature | Example | Use Case | |
|---|---|---|---|---|
| Primitives | string | let name: string = "Masum"; | Text data | |
number | let age: number = 25; | Numbers (int/float) | ||
boolean | let isActive: boolean = true; | True/false | ||
bigint | let big: bigint = 123n; | Arbitrary large integers | ||
symbol | let id: symbol = Symbol("id"); | Unique IDs | ||
null | let empty: null = null; | Explicit empty value | ||
undefined | let notDef: undefined = undefined; | Variable not initialized | ||
| Special | any | let anything: any = "hi"; | Opt-out of type checking (unsafe) | |
unknown | let value: unknown = "hi"; | Safer alternative to any | ||
never | function fail(): never { throw new Error("x"); } | Functions that never return | ||
void | function log(msg: string): void {} | No return value | ||
| Objects | object | let obj: object = { x: 10 }; | Non-primitive types | |
| Interface | interface User { id: number; name: string } | Define object shape | ||
| Class | class Person { constructor(public n: string) {} } | OOP style | ||
| Type alias | type Point = { x: number; y: number }; | Custom named type | ||
| Collections | Array | let nums: number[] = [1,2,3]; | Ordered lists | |
Tuple | let p: [string, number] = ["Alice", 30]; | Fixed-length array | ||
ReadonlyArray | let 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 types | let dir: "north" | "south"; | Restrict to exact values | ||
| Enums & Generics | Enum | enum Role { Admin, User } | Named constants | |
| Generics | function id<T>(x: T): T { return x; } | Reusable, type-safe | ||
| Utility Types | Partial<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 | ||
| Functions | Function type | let add: (a:number,b:number)=>number; | Explicit function typing | |
| Constructor type | type 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
-
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 -
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: numberYou didn’t write
: numberafter the function, but TypeScript knows the result ofa + bis a number. -
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 numberIf you mix types:
let mixed = [1, "hello", true];
// inferred as (string | number | boolean)[] -
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 aMouseEvent. -
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
stringandnumber). - So
Impossiblebecomesnever(an impossible type).
Union vs Intersection
| Feature | Union (|) – OR | Intersection (&) – AND |
|---|---|---|
| Meaning | Value can be one of many types | Value must satisfy all types |
| Usage | Flexibility (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)
UserIdis just an alias forstring | number.- This avoids repeating
string | numbereverywhere.
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
- Definition
- A
typegives a new name to any type (primitive, union, intersection, object, etc.). - An
interfaceis specifically used to describe the shape of an object (or function, class, etc.).
- 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 };
- 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'
- Other Type Features
- Type aliases can represent:
- Union types
type ID = string | number; - Tuples
type PointTuple = [number, number]; - Primitive type
type Name = string;
- Union types
- Interfaces cannot represent unions, primitives, or tuples. They are only for object shapes, function types, or classes.
- 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
| Feature | Interface | Type Alias |
|---|---|---|
| Object shape | ✅ Yes | ✅ Yes |
| Extend / Inherit | ✅ Yes (extends) | ✅ Yes (&) |
| Declaration merging | ✅ Yes | ❌ No |
| Union / Intersection | ❌ No | ✅ Yes |
| Primitive / Tuple alias | ❌ No | ✅ Yes |
| Flexibility | Less flexible | More flexible |