TypeScript Types
Understand the rich set of types in TypeScript for defining data structures and ensuring type safety.
TypeScript Types
Master TypeScript types with free flashcards and spaced repetition practice. This lesson covers primitive types, union and intersection types, type inference, literal types, and advanced type manipulationβessential concepts for building type-safe TypeScript applications.
Welcome to TypeScript Types π»
TypeScript's type system is the heart of what makes it powerful. Understanding types transforms JavaScript from a dynamically-typed language into one that catches errors at compile time, improves code documentation, and enables better IDE support. Whether you're building a small script or a large-scale application, mastering TypeScript types is fundamental to writing maintainable code.
Core Concepts
Primitive Types π€
TypeScript includes several primitive types that form the foundation of the type system:
String, Number, and Boolean are the most common:
let username: string = "Alice";
let age: number = 30;
let isActive: boolean = true;
π‘ Tip: TypeScript uses lowercase string, number, and boolean for primitives, not the capitalized String, Number, Boolean object wrappers.
Undefined and Null represent absence of value:
let notAssigned: undefined = undefined;
let emptyValue: null = null;
By default, null and undefined are subtypes of all types, but with strictNullChecks enabled (recommended), you must explicitly include them in your type annotations.
Symbol and BigInt are less commonly used but important:
let uniqueId: symbol = Symbol("id");
let largeNumber: bigint = 9007199254740991n;
Type Annotations vs Type Inference π―
TypeScript can automatically infer types based on assigned values:
// Explicit annotation
let explicitNumber: number = 42;
// Type inference (TypeScript infers number)
let inferredNumber = 42;
When to use annotations:
- Function parameters (required)
- When declaring variables without immediate initialization
- When you want to be explicit for documentation purposes
- When the inferred type is too broad
When inference works well:
- Variable initialization with literals
- Return types (though explicit is often better for public APIs)
- Simple assignments
Arrays and Tuples π
Arrays can be typed in two ways:
// Bracket notation
let numbers: number[] = [1, 2, 3, 4];
// Generic notation
let strings: Array<string> = ["a", "b", "c"];
Both are equivalent, but bracket notation is more concise and commonly preferred.
Tuples are fixed-length arrays with specific types for each position:
// Tuple: [string, number]
let person: [string, number] = ["Alice", 30];
// Accessing tuple elements
let name = person[0]; // string
let age = person[1]; // number
// Optional tuple elements
let coordinate: [number, number, number?] = [10, 20];
β οΈ Common Mistake: Tuples lose their structure when you use array methods like push(). TypeScript's type system doesn't prevent runtime modification beyond the declared length.
Union Types β‘
Union types allow a value to be one of several types using the | operator:
let id: string | number;
id = "ABC123"; // Valid
id = 123; // Valid
id = true; // Error: boolean is not assignable
Type Narrowing helps TypeScript understand which specific type you're working with:
function printId(id: string | number) {
if (typeof id === "string") {
// TypeScript knows id is string here
console.log(id.toUpperCase());
} else {
// TypeScript knows id is number here
console.log(id.toFixed(2));
}
}
Union type patterns:
| Pattern | Example | Use Case |
|---|---|---|
| Primitive unions | string | number | Flexible IDs, form values |
| Literal unions | "left" | "right" | "center" | Restricted string values |
| Nullable types | string | null | Optional values with strict null checks |
Intersection Types π
Intersection types combine multiple types using the & operator:
type HasName = { name: string };
type HasAge = { age: number };
type Person = HasName & HasAge;
const person: Person = {
name: "Alice",
age: 30
}; // Must have both properties
Intersections are particularly useful for mixing behaviors:
type Loggable = {
log: () => void;
};
type Serializable = {
serialize: () => string;
};
type LoggableSerializable = Loggable & Serializable;
const obj: LoggableSerializable = {
log() { console.log("logging"); },
serialize() { return JSON.stringify(this); }
};
π§ Memory Device: Think of Union as "OR" (can be this OR that) and Intersection as "AND" (must be this AND that).
Literal Types π
Literal types are exact values, not just broader categories:
// String literal types
let alignment: "left" | "center" | "right";
alignment = "left"; // Valid
alignment = "top"; // Error
// Numeric literal types
let diceRoll: 1 | 2 | 3 | 4 | 5 | 6;
// Boolean literal types
let isTrue: true; // Only accepts true, not false
Literal types in functions create powerful type-safe APIs:
function setAlignment(align: "left" | "center" | "right") {
// Implementation
}
setAlignment("left"); // Valid
setAlignment("top"); // Error: Argument not assignable
const userAlign = "center";
setAlignment(userAlign); // Valid: TypeScript infers literal type
Const assertions help preserve literal types:
// Without const assertion
let config = {
port: 3000, // Type: number
host: "localhost" // Type: string
};
// With const assertion
let configConst = {
port: 3000, // Type: 3000
host: "localhost" // Type: "localhost"
} as const;
Type Aliases π·οΈ
Type aliases create reusable type definitions:
type UserId = string | number;
type Point = { x: number; y: number };
type Callback = (data: string) => void;
function processUser(id: UserId) {
// Use the alias
}
let origin: Point = { x: 0, y: 0 };
Aliases vs Interfaces: Type aliases are more flexible (can represent unions, primitives, tuples), while interfaces are better for object shapes that might be extended:
// Type alias: Can represent anything
type StringOrNumber = string | number;
type Coordinate = [number, number];
// Interface: Better for objects
interface User {
name: string;
age: number;
}
// Interfaces can be extended
interface Employee extends User {
employeeId: string;
}
Any, Unknown, and Never π«
Any disables type checking (use sparingly):
let anything: any = "hello";
anything = 42; // No error
anything.someMethod(); // No error, but might fail at runtime
β οΈ Warning: any defeats the purpose of TypeScript. Use it only when absolutely necessary (gradual migration, third-party libraries without types).
Unknown is type-safe alternative to any:
let uncertain: unknown = "hello";
// Error: Must narrow type first
// uncertain.toUpperCase();
if (typeof uncertain === "string") {
uncertain.toUpperCase(); // Valid after narrowing
}
Never represents values that never occur:
// Function that never returns
function throwError(message: string): never {
throw new Error(message);
}
// Exhaustive type checking
type Shape = "circle" | "square";
function getArea(shape: Shape): number {
switch (shape) {
case "circle":
return Math.PI * 10 * 10;
case "square":
return 10 * 10;
default:
// If we add a new shape and forget to handle it,
// TypeScript will error here
const exhaustive: never = shape;
throw new Error(`Unhandled shape: ${exhaustive}`);
}
}
Void and Object Types π¨
Void represents absence of a return value:
function logMessage(message: string): void {
console.log(message);
// No return statement
}
let result: void = undefined; // void is only assignable to undefined
Object types can be defined inline or as aliases:
// Inline object type
function printUser(user: { name: string; age: number }) {
console.log(`${user.name} is ${user.age}`);
}
// Optional properties
type Config = {
host: string;
port?: number; // Optional with ?
timeout?: number;
};
// Readonly properties
type ImmutablePoint = {
readonly x: number;
readonly y: number;
};
let point: ImmutablePoint = { x: 10, y: 20 };
point.x = 30; // Error: Cannot assign to readonly property
Detailed Examples
Example 1: Building a Type-Safe API Response Handler
Let's create a robust API response handler using TypeScript types:
// Define possible response statuses as literal types
type Status = "success" | "error" | "loading";
// Generic response type with conditional data
type ApiResponse<T> =
| { status: "success"; data: T }
| { status: "error"; error: string }
| { status: "loading" };
// User data structure
type User = {
id: number;
username: string;
email: string;
};
// Type-safe handler function
function handleResponse(response: ApiResponse<User>) {
switch (response.status) {
case "success":
// TypeScript knows 'data' exists here
console.log(`User: ${response.data.username}`);
break;
case "error":
// TypeScript knows 'error' exists here
console.error(`Error: ${response.error}`);
break;
case "loading":
// TypeScript knows only 'status' exists here
console.log("Loading...");
break;
}
}
// Usage examples
const successResponse: ApiResponse<User> = {
status: "success",
data: { id: 1, username: "alice", email: "alice@example.com" }
};
const errorResponse: ApiResponse<User> = {
status: "error",
error: "Network timeout"
};
handleResponse(successResponse);
handleResponse(errorResponse);
Why this works: The discriminated union pattern uses the status property to help TypeScript narrow the type. Once you check response.status, TypeScript knows exactly which properties are available.
Example 2: Type Guards for Runtime Validation
Type guards help TypeScript narrow types based on runtime checks:
// Define types for different shapes
type Circle = {
kind: "circle";
radius: number;
};
type Rectangle = {
kind: "rectangle";
width: number;
height: number;
};
type Shape = Circle | Rectangle;
// User-defined type guard
function isCircle(shape: Shape): shape is Circle {
return shape.kind === "circle";
}
// Calculate area with type narrowing
function calculateArea(shape: Shape): number {
if (isCircle(shape)) {
// TypeScript knows shape is Circle here
return Math.PI * shape.radius ** 2;
} else {
// TypeScript knows shape is Rectangle here
return shape.width * shape.height;
}
}
// Usage
const myCircle: Circle = { kind: "circle", radius: 10 };
const myRectangle: Rectangle = { kind: "rectangle", width: 5, height: 8 };
console.log(calculateArea(myCircle)); // ~314.16
console.log(calculateArea(myRectangle)); // 40
Key insight: The shape is Circle return type is a type predicate that tells TypeScript the function performs a type check.
Example 3: Working with Optional and Nullable Types
Handling optional and null values safely:
// User type with optional properties
type UserProfile = {
id: number;
username: string;
email?: string; // Optional: may be undefined
phoneNumber: string | null; // Nullable: explicitly null or string
};
// Function using optional chaining
function getEmailDomain(user: UserProfile): string | undefined {
// Optional chaining returns undefined if email doesn't exist
return user.email?.split("@")[1];
}
// Function using nullish coalescing
function getContactInfo(user: UserProfile): string {
// ?? returns right side only if left is null or undefined
const phone = user.phoneNumber ?? "No phone provided";
const email = user.email ?? "No email provided";
return `Phone: ${phone}, Email: ${email}`;
}
// Non-null assertion operator (use cautiously)
function forceGetEmail(user: UserProfile): string {
// ! tells TypeScript "I'm sure this exists"
return user.email!.toLowerCase();
// β οΈ Dangerous: throws runtime error if email is undefined
}
// Safe handling with type narrowing
function safeGetEmail(user: UserProfile): string {
if (user.email !== undefined) {
return user.email.toLowerCase();
}
return "no-email@example.com";
}
// Example usage
const user1: UserProfile = {
id: 1,
username: "alice",
email: "alice@example.com",
phoneNumber: "+1234567890"
};
const user2: UserProfile = {
id: 2,
username: "bob",
phoneNumber: null
};
console.log(getEmailDomain(user1)); // "example.com"
console.log(getEmailDomain(user2)); // undefined
console.log(getContactInfo(user2)); // "Phone: No phone provided, Email: No email provided"
Best practices shown:
- Use
?for optional properties that may not exist - Use
| nullfor values that are explicitly nullable - Use
?.for safe property access - Use
??for default values - Avoid
!unless you're absolutely certain
Example 4: Template Literal Types (Advanced)
TypeScript can create types from string patterns:
// Build CSS property types
type CSSUnit = "px" | "em" | "rem" | "%";
type CSSValue<T extends number> = `${T}${CSSUnit}`;
// Valid values
const width: CSSValue<100> = "100px";
const fontSize: CSSValue<1.5> = "1.5rem";
// HTTP method types
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type Endpoint = "/users" | "/posts" | "/comments";
type APIRoute = `${HTTPMethod} ${Endpoint}`;
// Type-safe route definition
function makeRequest(route: APIRoute) {
console.log(`Making request: ${route}`);
}
makeRequest("GET /users"); // Valid
makeRequest("POST /posts"); // Valid
// makeRequest("PATCH /users"); // Error: not assignable
// Event naming convention
type EventName = "click" | "focus" | "blur";
type ElementId = "button" | "input" | "form";
type Handler = `on${Capitalize<EventName>}_${ElementId}`;
const handler: Handler = "onClick_button"; // Valid
When to use: Template literal types are powerful for enforcing naming conventions, building type-safe APIs, and creating precise string-based types.
Common Mistakes β οΈ
Mistake 1: Using any Instead of unknown
β Wrong:
function processData(data: any) {
return data.toUpperCase(); // No type safety
}
β Correct:
function processData(data: unknown) {
if (typeof data === "string") {
return data.toUpperCase(); // Type-safe
}
throw new Error("Expected string");
}
Mistake 2: Forgetting Strict Null Checks
β Wrong:
function getLength(str: string) {
return str.length; // Runtime error if str is null
}
getLength(null as any); // Crashes
β Correct:
function getLength(str: string | null): number {
return str?.length ?? 0; // Safe handling
}
Mistake 3: Overusing Type Assertions
β Wrong:
const element = document.getElementById("myId") as HTMLInputElement;
element.value = "test"; // Might fail if element is actually a div
β Correct:
const element = document.getElementById("myId");
if (element instanceof HTMLInputElement) {
element.value = "test"; // Type-safe
}
Mistake 4: Not Using Discriminated Unions
β Wrong:
type Response = {
data?: User;
error?: string;
};
// Ambiguous: both could be present or absent
β Correct:
type Response =
| { success: true; data: User }
| { success: false; error: string };
// Clear: exactly one case applies
Mistake 5: Ignoring Type Inference
β Wrong (over-annotation):
const numbers: number[] = [1, 2, 3].map((n: number): number => n * 2);
β Correct (let inference work):
const numbers = [1, 2, 3].map(n => n * 2); // TypeScript infers everything
Key Takeaways π―
π TypeScript Types Quick Reference
| Concept | Syntax | Use Case |
|---|---|---|
| Primitive Types | string, number, boolean | Basic values |
| Arrays | type[] or Array<type> | Lists of values |
| Tuples | [string, number] | Fixed-length, mixed types |
| Union Types | string | number | One of several types |
| Intersection Types | A & B | Combine multiple types |
| Literal Types | "left" | "right" | Exact values only |
| Type Aliases | type Name = ... | Reusable type definitions |
| Optional | property?: type | May be undefined |
| Nullable | type | null | Explicitly null or value |
| Unknown | unknown | Type-safe any |
| Never | never | Values that never occur |
| Void | void | No return value |
Golden Rules:
- π― Be explicit with public APIs - use type annotations for function signatures
- π Trust inference for locals - let TypeScript infer variable types when obvious
- π« Avoid
any- useunknownwhen type is truly uncertain - β
Use strict mode - enable
strict: truein tsconfig.json - π‘οΈ Narrow types - use type guards to refine union types safely
- π Use literal types - restrict strings/numbers to specific values when possible
- π Prefer unions for "or" - use
|for alternatives - π€ Prefer intersections for "and" - use
&to combine types
Further Study π
Ready to deepen your TypeScript type knowledge? Explore these resources:
TypeScript Official Handbook - Everyday Types: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html - Comprehensive guide to common TypeScript types with interactive examples
TypeScript Deep Dive: https://basarat.gitbook.io/typescript/ - Free online book covering advanced type system features and patterns
Type Challenges: https://github.com/type-challenges/type-challenges - Practice TypeScript types with progressively difficult challenges
Continue your journey by exploring Interfaces and Type Aliases in the next lesson, where you'll learn about extending types, index signatures, and when to choose interfaces over type aliases! π