Skip to main content

Core Principles

The golden rule: Avoid using any type at all costs. Using any defeats the purpose of TypeScript and the exercises.
The difficulty of exercises grows quickly as you progress. Each exercise builds upon concepts from previous ones, so it’s important to understand each solution before moving forward.

General Best Practices

Start Simple

When facing a complex type problem, break it down:
  1. Read the exercise description carefully - Understand what the exercise asks you to do
  2. Look at the data structures - Examine the interfaces and the actual data being used
  3. Identify the type errors - Read the TypeScript error messages to understand what’s wrong
  4. Start with the simplest solution - Don’t over-engineer, TypeScript often has built-in utilities for common patterns

Understanding Type Errors

TypeScript error messages can be verbose, but they’re precise:
Read error messages from bottom to top. The last error often reveals the root cause, while earlier errors may be consequences of that root issue.
// Example: Property 'role' does not exist on type 'Person'
export function logPerson(person: Person) {
    let additionalInformation: string;
    if (person.role) { // Error here!
        additionalInformation = person.role;
    }
}
This error tells you that TypeScript doesn’t know that person has a role property. You need to narrow the type using type guards.

Common Pitfalls & Solutions

Pitfall 1: Not Using Type Guards

Problem: Trying to access properties that don’t exist on union types.
When you have a union type like User | Admin, TypeScript only allows you to access properties that exist on both types. Solution: Use type guards to narrow the type:
// Using 'in' operator (Exercise 3)
if ('role' in person) {
    console.log(person.role); // TypeScript knows it's Admin
}

// Using type predicates (Exercise 4)
function isAdmin(person: Person): person is Admin {
    return person.type === 'admin';
}

if (isAdmin(person)) {
    console.log(person.role); // Type is narrowed to Admin
}

Pitfall 2: Duplicating Type Structures

Problem: Manually rewriting all properties instead of using utility types.
Solution: Leverage TypeScript’s utility types:
// Don't do this:
type UserCriteria = {
    name?: string;
    age?: number;
    occupation?: string;
}

// Do this instead (Exercise 5):
type UserCriteria = Partial<User>;

// For complex cases (Exercise 8):
type PowerUser = Omit<User, 'type'> & Omit<Admin, 'type'> & {
    type: 'powerUser';
};

Pitfall 3: Over-Constraining Generic Types

Problem: Making generics too specific, reducing their reusability.
Solution: Keep generics flexible:
// Too specific:
function swap(v1: Person, v2: Person): [Person, Person] {
    return [v2, v1];
}

// Better - works with any types (Exercise 7):
function swap<T, U>(v1: T, v2: U): [U, T] {
    return [v2, v1];
}

Pitfall 4: Not Understanding Module Systems

Exercises 11-13 deal with type declarations and module augmentation. These concepts are crucial for working with third-party libraries.
Key concepts:
  • .d.ts files contain only type information, no runtime code
  • Use declare module to provide types for JavaScript libraries
  • Module augmentation lets you extend existing type declarations
// Type declaration file (Exercise 11)
declare module 'str-utils' {
    export function strReverse(str: string): string;
    export function strToLower(str: string): string;
}

// Module augmentation (Exercise 13)
declare module 'date-wizard' {
    interface DateDetails {
        hours: number;    // Adding missing properties
        minutes: number;
        seconds: number;
    }
}

Debugging Strategies

Strategy 1: Hover for Type Information

Hover over variables and functions in your IDE to see what TypeScript infers. This helps you understand where types diverge from your expectations.

Strategy 2: Use Type Assertions Carefully

// Use 'as const' for literal types
const config = {
    apiUrl: 'https://api.example.com',
    timeout: 5000
} as const; // Makes properties readonly and literal

// Avoid 'as any' - it defeats the purpose
const value = someFunction() as any; // Don't do this!

Strategy 3: Incremental Typing

  1. Replace unknown or any with a basic type
  2. Run TypeScript to see what errors appear
  3. Refine the type based on errors and requirements
  4. Repeat until all errors are resolved

Strategy 4: Read the Hints

Every exercise includes a link to relevant TypeScript documentation:
// In case you are stuck:
// https://www.typescriptlang.org/docs/handbook/...
These links point to exactly the concepts you need. Read them thoroughly before attempting complex solutions.

Advanced TypeScript Tips

Working with Generics (Exercises 7, 10, 14)

Generics make your code reusable:
// Simple generic
function identity<T>(arg: T): T {
    return arg;
}

// Generic with constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

// Generic with multiple type parameters (Exercise 10)
function promisify<T>(
    fn: (callback: (response: ApiResponse<T>) => void) => void
): () => Promise<T> {
    return () => new Promise((resolve, reject) => {
        fn((response) => {
            if (response.status === 'success') {
                resolve(response.data);
            } else {
                reject(new Error(response.error));
            }
        });
    });
}

Mapped Types (Exercise 15)

For advanced type transformations:
// Make all properties of T have type U
type ObjectWithProps<T> = {
    [K in keyof T]: T[K];
};

// Conditional types
type NonNullableProps<T> = {
    [K in keyof T]: NonNullable<T[K]>;
};

Function Overloads (Exercise 14)

Function overloads let you specify multiple function signatures for different use cases.
// Declare overloads
function map<T, U>(mapper: (item: T) => U, input: T[]): U[];
function map<T, U>(mapper: (item: T) => U): (input: T[]) => U[];
function map(): typeof map;

// Implementation signature
function map(mapper?, input?) {
    // Implementation here
}

TypeScript Configuration Tips

The exercises use strict TypeScript settings. This catches more errors but requires more precise typing.
Key compiler options you’ll encounter:
  • strict: true - Enables all strict type checking options
  • noImplicitAny: true - Flags implicit any types
  • strictNullChecks: true - null and undefined must be explicitly handled
  • strictFunctionTypes: true - Function parameters are checked contravariantly

Performance Tips

TypeScript type checking happens at compile time, so type complexity doesn’t affect runtime performance.
However, complex types can slow down IDE performance:
  • Avoid deeply nested conditional types when simpler solutions exist
  • Use type aliases to make complex types more readable
  • Break down complex types into smaller, reusable pieces

Getting Unstuck

  1. Check if you’re using the correct TypeScript version
  2. Make sure all required types are imported
  3. Look at the exercise solution file (index.solution.ts) for hints about the approach
  4. Simplify the problem: comment out code until errors are manageable, then add back gradually
TypeScript is a static type system. If your types are wrong, you haven’t truly solved the exercise. The goal is to make TypeScript understand your code, not just to make it run.
  • Revisit the exercise requirements
  • Check if you’re using any somewhere (forbidden!)
  • Verify your solution matches the expected behavior
  1. Copy the error into a search engine with “TypeScript” prefix
  2. Read the linked documentation in the exercise
  3. Use the TypeScript Playground to experiment with minimal reproductions
  4. Break down complex types to see where the mismatch occurs

Key Takeaways

Never Use Any

The any type disables TypeScript’s type checking. It’s explicitly forbidden in these exercises.

Read Error Messages

TypeScript errors are precise. They tell you exactly what’s wrong and often suggest solutions.

Use Built-in Utilities

TypeScript provides utility types like Partial, Pick, Omit, and more. Don’t reinvent them.

Type Guards Are Essential

Narrowing types with guards is fundamental to working with union types effectively.
Remember: The goal isn’t just to complete exercises, but to deeply understand TypeScript’s type system. Take your time and experiment!

Build docs developers (and LLMs) love