Types and Interfaces in TypeScript
TypeScript provides powerful abstractions for modeling data structures, enforcing strict contracts, and enabling reusability across software applications. This lecture explores the core mechanisms of data shape modeling in TypeScript: Type Aliases, Interfaces, Union Types, Intersection Types, Literal Types, and property modifiers such as Optional (?) and Readonly (readonly).
1. Motivation: The Problem of Redundant Type Signatures
When building applications in JavaScript and migrating them to TypeScript, developers often write complex inline type definitions directly within function parameters. While functional for single-use cases, inline type signatures lead to duplicate code, poor maintainability, and broken DRY (Don't Repeat Yourself) principles when the same data shape is required across multiple functions.
The Naive Approach: Inline Type Annotations
Consider a scenario where multiple functions handle a tea order object with identical structural requirements:
// Naive implementation with repeated inline signatures
function makeChai(order: { type: string; sugar: number; strong: boolean }) {
console.log(order);
}
function serveChai(order: { type: string; sugar: number; strong: boolean }) {
console.log(order);
}
Problem Analysis
- Signature Duplication: The structural contract
{ type: string; sugar: number; strong: boolean }is duplicated across functions. - Refactoring Overhead: Updating the order properties (e.g., adding a
milkproperty) requires modifying every inline type definition manually across the codebase. - Conceptual Parallel: Just as Data Structures and Algorithms (DSA) define standardized structural nodes (e.g., a Linked List node containing a data payload and pointer references), application data requires explicit structural contracts.
2. Type Aliases (type)
A Type Alias allows developers to assign a custom name to any type signature, whether it is a primitive type, an object structure, a union, or a complex signature.
Vertical Flow of Type Extraction
Refactored Code with Type Aliases
// Define a central type alias
type ChaiOrder = {
type: string;
sugar: number;
strong: boolean;
};
// Refactored clean functions
function makeChai(order: ChaiOrder) {
console.log(order);
}
function serveChai(order: ChaiOrder) {
console.log(order);
}
Note: Type aliases do not create new types; they create a new name to refer to an existing type signature, significantly boosting code readability and domain semantics.
3. Interfaces (interface)
An Interface is another fundamental contract-defining construct in TypeScript. Unlike Type Aliases, which use an assignment operator (=), interfaces use explicit structural declaration syntax suited for object-oriented design and contract enforcement.
Basic Syntax Comparison
Type Alias Syntax
type TRecipe = {
water: number;
milk: number;
};
Interface Syntax
interface IRecipe {
water: number;
milk: number;
}
4. Class Implementation Nuances: Types vs. Interfaces
Classes in TypeScript can implement structural contracts defined by both type aliases and interface declarations, but key edge cases dictate when each should be used.
Class Implementing an Object Type Alias
When a type alias defines a standard object structure with statically known members, a class can implement it without issues:
type TRecipe = {
water: number;
milk: number;
};
class MasalaChai implements TRecipe {
water = 100;
milk = 50;
}
Where Type Aliases Fail in Class Contracts
Classes cannot implement non-object type aliases, union types, or primitive aliases. Attempting to do so triggers a TypeScript compiler error.
Case 1: Union Types in Class Contracts
type CupSize = "small" | "large";
// ❌ COMPILER ERROR: Class can only implement an object type or intersection of object types with statically known members.
class Chai implements CupSize {
size: "small" | "large" = "large";
}
Case 2: Primitive / Literal Response Unions
type ResponseType = { ok: true } | { ok: false };
// ❌ COMPILER ERROR: Union type cannot be implemented by a class directly
class MyResponse implements ResponseType {
ok: boolean = true;
}
Solution: Interface Design Pattern for Classes
To enforce contracts on classes cleanly, wrap the primitive or literal choices inside an explicit object interface:
interface ICupConfig {
size: "small" | "large";
}
class Chai implements ICupConfig {
size: "small" | "large" = "large";
}
Industry Best Practice: Use Interfaces by default when defining contracts for OOP classes (e.g., in frameworks like NestJS or Angular). Use Type Aliases for complex unions, intersections, primitives, and dynamic data manipulation.
5. Union Types (|) and Literal Types
Union Types (|)
A Union Type defines a variable or parameter that can hold values matching one of several types or specific explicit values.
Literal Types
A Literal Type restricts a variable to specific concrete primitive values rather than broader type categories (like string or number).
// Definition of a Literal Union Type
type TType = "masala" | "ginger" | "lemon";
function orderChai(tea: TType) {
console.log(`Preparing: ${tea}`);
}
// Valid invocations
orderChai("masala");
orderChai("ginger");
// ❌ COMPILER ERROR: Argument of type '"coffee"' is not assignable to parameter of type 'TType'.
// orderChai("coffee");
6. Intersection Types (&)
An Intersection Type combines multiple type signatures into a single unified type. An object of an intersection type must satisfy all combined type contracts simultaneously.
Combining Base and Extension Types
type BaseChai = {
teaLeaves: number;
};
type ExtraIngredients = {
masala: number;
};
// Intersection: combines BaseChai AND ExtraIngredients
type MasalaChaiType = BaseChai & ExtraIngredients;
// Correct usage containing ALL required properties
const myCup: MasalaChaiType = {
teaLeaves: 2,
masala: 1
};
If any property from either combined type is omitted, TypeScript throws a compile-time error:
// ❌ COMPILER ERROR: Property 'masala' is missing in type '{ teaLeaves: number }' but required in type 'ExtraIngredients'.
const incompleteCup: MasalaChaiType = {
teaLeaves: 2
};
7. Modifier Keywords: Optional (?) and Readonly (readonly)
TypeScript offers property-level modifiers to control mutability and existence contracts within types and interfaces.
Optional Properties (?)
Appending a ? after a property key marks it as optional, allowing it to evaluate to its specified type or undefined.
type User = {
username: string;
bio?: string; // Optional property
};
// Valid without bio
const u1: User = {
username: "hitesh"
};
// Valid with bio provided as a string
const u2: User = {
username: "hitesh",
bio: "hitesh.ai"
};
Readonly Properties (readonly)
The readonly modifier prevents reassignment of a property after its initial object instantiation.
type AppConfig = {
readonly appName: string;
version: number;
};
const cfg: AppConfig = {
appName: "Masterji",
version: 1
};
// Valid property update
cfg.version = 2;
// ❌ COMPILER ERROR: Cannot assign to 'appName' because it is a read-only property.
// cfg.appName = "ChaiCode";
8. Tabular Synthesis: Type Aliases vs. Interfaces
9. Complete Flow Walkthrough: Type Operations Lifecycle
Summary and Key Takeaways
- Eliminate Duplication: Replace repeated inline function parameter shapes with central
typealiases orinterfacedefinitions to maintain DRY principles. - Class Contracts: Classes can implement basic object-shaped
typealiases, but they cannot implement union or literal types. Useinterfacedefinitions for class contracts. - Literal Types: Restrict parameters to exact primitive values (e.g.,
"small" | "large") to catch invalid inputs at compile-time instead of runtime. - Unions (
|) vs. Intersections (&):- Union (
|) represents an OR relationship (select one valid structural state/value). - Intersection (
&) represents an AND relationship (merge and enforce all combined properties).
- Union (
- Optional Properties (
?): Allows property keys to be omitted safely without breaking type checking. - Readonly Properties (
readonly): Enforces immutability on specified object properties after initialization, preventing unintended runtime reassignments.