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();