read

Moving from Swift to TypeScript? Here are the key concepts I learned while diving into a React codebase, with the help of Claude Code.

And I should add another tip: Claude Code makes a great teacher for exploring a new codebase or language.

Interfaces: Just Compile-Time Promises

Interfaces are like Swift protocols - they define shapes but don’t guarantee runtime data:

interface User {
  id: string;
  email: string;
}

API might return { id: 123, email: null } 💥 crash with null string!

Arrow Functions as Class Properties

This pattern confused me initially:

class AuthService {
  login: (email: string, password: string) => Promise<User> = async (email, password) => {
    return this.apiCall('/login', { email, password });
  };
}

Why use arrow functions? They preserve this context:

const auth = new AuthService();
const loginFn = auth.login;  // Detached from class
loginFn('[email protected]', 'pass123');  // Still works! 'this' preserved

Type Extraction from Interfaces

Building on the previous example, we can extract types from interfaces to avoid duplication:

interface AuthAPI {
  login(email: string, password: string): Promise<User>;
}

class AuthService implements AuthAPI {
  // AuthAPI['login'] extracts the complete function signature at compile-time:
  // (email: string, password: string) => Promise<User>
  login: AuthAPI['login'] = async (email, password) => {
    return this.apiCall('/login', { email, password });
  };
}

This ensures your implementation stays synchronized with the interface - if the interface changes, TypeScript immediately flags the mismatch.

Classes and Constructors

TypeScript classes are similar to Swift, with constructor replacing init:

class UserService {
  // Optional to declare properties separately!
  constructor(
    readonly id: string,         // like Swift 'let'
    public name: string,         // like Swift 'var'
    private apiKey: string       // like Swift 'private var'
  ) {}  // No body needed - properties auto-assigned!
}

React Query Basics

useQuery from TanStack Query manages API calls with automatic caching, isLoading state, and preventing UI flickers:

const { data, isLoading, error } = useQuery({
  queryKey: ['user', userId],        // Cache key - shared across components
  queryFn: async ({ signal }) => {
    const res = await fetch(`/api/users/${userId}`, { signal });
    return res.json();
  },
  refetchInterval: 30_000,           // Auto-refresh every 30s
  retry: 3,                          // Retry failed requests 3 times
  retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
  staleTime: 5000,                   // Consider data fresh for 5s
});

Key benefits:

  • No flicker: When navigating back, shows cached data instantly while refetching in background
  • Request deduplication: Multiple components using same queryKey share one request
  • Smart retries: Exponential backoff prevents hammering failed endpoints
  • The signal parameter enables request cancellation when components unmount

The Spread Operator

The ... operator copies properties immutably:

// Object spreading, adding a `name` property
const updated = { ...original, name: 'New Name' };

// Common in React Query's select
select: (data) => ({
  ...data,
  // Replaces data.items
  items: data.items.map(item => ({
    ...item,
    formatted: formatDate(item.createdAt)
  }))
})

Runtime Validation with Zod

Since TypeScript can’t validate runtime data, use Zod:

import { z } from 'zod';

const UserSchema = z.object({
  id: z.string(),
  age: z.number().min(0)
});

// Validates at runtime
try {
  const user = UserSchema.parse(apiResponse);
  // user is guaranteed to match the schema
} catch (error) {
  console.error('Invalid API response');
}

Practical Service Pattern

Here’s a clean pattern for API services using arrow functions:

interface TodoAPI {
  getTodos(): Promise<Todo[]>;
  createTodo(title: string): Promise<Todo>;
}

class TodoService implements TodoAPI {
  private baseUrl = '/api/todos';
  
  // Arrow functions preserve 'this' when methods are passed around
  getTodos: TodoAPI['getTodos'] = async () => {
    const res = await fetch(this.baseUrl);
    return res.json();
  };
  
  createTodo: TodoAPI['createTodo'] = async (title) => {
    const res = await fetch(this.baseUrl, {
      method: 'POST',
      body: JSON.stringify({ title })
    });
    return res.json();
  };
}

export const todoService = new TodoService();

Image

@samwize

¯\_(ツ)_/¯

Back to Home