You are viewing a preview of this lesson. Sign in to start learning
Back to TypeScript

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:

PatternExampleUse Case
Primitive unionsstring | numberFlexible IDs, form values
Literal unions"left" | "right" | "center"Restricted string values
Nullable typesstring | nullOptional 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 | null for 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

ConceptSyntaxUse Case
Primitive Typesstring, number, booleanBasic values
Arraystype[] or Array<type>Lists of values
Tuples[string, number]Fixed-length, mixed types
Union Typesstring | numberOne of several types
Intersection TypesA & BCombine multiple types
Literal Types"left" | "right"Exact values only
Type Aliasestype Name = ...Reusable type definitions
Optionalproperty?: typeMay be undefined
Nullabletype | nullExplicitly null or value
UnknownunknownType-safe any
NeverneverValues that never occur
VoidvoidNo return value

Golden Rules:

  1. 🎯 Be explicit with public APIs - use type annotations for function signatures
  2. πŸ” Trust inference for locals - let TypeScript infer variable types when obvious
  3. 🚫 Avoid any - use unknown when type is truly uncertain
  4. βœ… Use strict mode - enable strict: true in tsconfig.json
  5. πŸ›‘οΈ Narrow types - use type guards to refine union types safely
  6. πŸ“ Use literal types - restrict strings/numbers to specific values when possible
  7. πŸ”— Prefer unions for "or" - use | for alternatives
  8. 🀝 Prefer intersections for "and" - use & to combine types

Further Study πŸ“š

Ready to deepen your TypeScript type knowledge? Explore these resources:

  1. TypeScript Official Handbook - Everyday Types: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html - Comprehensive guide to common TypeScript types with interactive examples

  2. TypeScript Deep Dive: https://basarat.gitbook.io/typescript/ - Free online book covering advanced type system features and patterns

  3. 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! πŸš€