Testing & TypeScript
Write robust tests and add type safety to applications
Testing and TypeScript in React
Master testing and TypeScript integration in React with free flashcards and spaced repetition practice. This lesson covers unit testing with Jest and React Testing Library, TypeScript configuration for React projects, type-safe component development, and end-to-end testing strategiesβessential skills for building robust, maintainable React applications.
Welcome to Testing & TypeScript π»π§ͺ
Welcome to one of the most crucial aspects of professional React development! Testing and TypeScript are not just "nice-to-have" additionsβthey're industry standards that separate hobby projects from production-ready applications. Testing ensures your components behave correctly and continue working as your codebase evolves, while TypeScript catches bugs before runtime and provides excellent developer experience through autocomplete and inline documentation.
In modern React teams, you'll rarely see a codebase without these tools. Companies like Airbnb, Slack, and Microsoft rely heavily on TypeScript and comprehensive testing to maintain their React applications. By the end of this lesson, you'll understand how to write type-safe React components, test them effectively, and integrate both practices into your development workflow.
Core Concepts: TypeScript in React π
What is TypeScript?
TypeScript is a superset of JavaScript that adds static type checking. Think of it as JavaScript with guardrailsβit catches errors at compile time rather than runtime, making your code more predictable and maintainable.
Key benefits for React:
- Type safety: Catch prop type mismatches before they reach production
- IntelliSense: Get autocomplete suggestions for props, state, and methods
- Refactoring confidence: Rename components or props knowing the compiler will catch all references
- Self-documenting code: Types serve as inline documentation
Setting Up TypeScript in React
For new projects, create a TypeScript React app:
npx create-react-app my-app --template typescript
For existing projects:
npm install --save typescript @types/react @types/react-dom
Then rename your .js files to .tsx (for files with JSX) or .ts (for pure TypeScript).
TypeScript Configuration
The tsconfig.json file controls TypeScript behavior:
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}
π‘ Tip: Start with "strict": true to enable all strict type-checking options. It's harder initially but prevents bad habits.
Typing React Components
Function Components with Props
import React from 'react';
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean; // Optional prop
variant?: 'primary' | 'secondary'; // Union type
}
const Button: React.FC<ButtonProps> = ({ label, onClick, disabled = false, variant = 'primary' }) => {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant}`}
>
{label}
</button>
);
};
export default Button;
π‘ Note: React.FC is optional and somewhat controversial. Many developers prefer explicit typing:
const Button = ({ label, onClick, disabled = false }: ButtonProps) => {
// implementation
};
Class Components
import React, { Component } from 'react';
interface CounterProps {
initialCount: number;
}
interface CounterState {
count: number;
}
class Counter extends Component<CounterProps, CounterState> {
constructor(props: CounterProps) {
super(props);
this.state = {
count: props.initialCount
};
}
increment = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>+</button>
</div>
);
}
}
Hooks with TypeScript
useState
// TypeScript infers the type
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string
// Explicit typing for complex types
interface User {
id: number;
name: string;
email: string;
}
const [user, setUser] = useState<User | null>(null);
useEffect
import { useEffect } from 'react';
useEffect(() => {
const timer = setTimeout(() => {
console.log('Delayed action');
}, 1000);
// Cleanup function is properly typed
return () => clearTimeout(timer);
}, []);
useRef
import { useRef } from 'react';
function TextInput() {
// Type the ref with HTMLInputElement
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
// TypeScript knows inputRef.current might be null
inputRef.current?.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus</button>
</div>
);
}
Custom Hooks
import { useState, useEffect } from 'react';
interface FetchState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
function useFetch<T>(url: string): FetchState<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then((data: T) => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
// Usage
interface Post {
id: number;
title: string;
body: string;
}
const { data, loading, error } = useFetch<Post[]>('/api/posts');
Core Concepts: Testing React Components π§ͺ
Testing Philosophy
Test what users see and do, not implementation details. Your tests should resemble how users interact with your application. This approach, championed by React Testing Library, leads to more maintainable tests.
π§ Testing Pyramid Mnemonic: "U.I. END"
β±β² END-TO-END (few)
β± β² User flows
β±βββββ²
β± β² INTEGRATION (some)
β± β² Component interaction
β±βββββββββββ²
β± β² UNIT (many)
β±βββββββββββββββ² Individual functions
Unit tests are the foundation, Integration tests verify components work together, END tests validate complete user journeys.
Setting Up Testing Environment
Jest comes pre-configured with Create React App. For React Testing Library:
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event
Basic Test Structure
Tests follow the AAA pattern: Arrange, Act, Assert.
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import Button from './Button';
describe('Button Component', () => {
it('renders with correct label', () => {
// Arrange
render(<Button label="Click me" onClick={() => {}} />);
// Act (implicit - just rendering)
// Assert
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick handler when clicked', () => {
// Arrange
const handleClick = jest.fn();
render(<Button label="Click me" onClick={handleClick} />);
// Act
fireEvent.click(screen.getByText('Click me'));
// Assert
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('is disabled when disabled prop is true', () => {
// Arrange & Act
render(<Button label="Click me" onClick={() => {}} disabled={true} />);
// Assert
expect(screen.getByText('Click me')).toBeDisabled();
});
});
Querying Elements
React Testing Library provides several query methods:
| Query Type | When to Use | Returns | Throws Error? |
|---|---|---|---|
getBy... | Element should exist | Element | Yes |
queryBy... | Element might not exist | Element or null | No |
findBy... | Async element (will appear) | Promise<Element> | Yes |
getAllBy... | Multiple elements should exist | Element[] | Yes |
Query selectors in priority order:
- getByRole: Accessible to assistive technologies
- getByLabelText: For form fields
- getByPlaceholderText: If no label
- getByText: Non-interactive elements
- getByTestId: Last resort
// Good: Using accessible queries
screen.getByRole('button', { name: /submit/i });
screen.getByLabelText('Email address');
// Avoid: Using test IDs unless necessary
screen.getByTestId('submit-button');
Testing Async Behavior
import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from './UserProfile';
test('loads and displays user data', async () => {
// Mock API call
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ name: 'John Doe', email: 'john@example.com' })
})
) as jest.Mock;
render(<UserProfile userId={1} />);
// Initially shows loading
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Wait for data to load
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
// Or use findBy (combines getBy + waitFor)
expect(await screen.findByText('john@example.com')).toBeInTheDocument();
});
Testing Hooks
Use @testing-library/react-hooks for testing custom hooks:
import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter';
test('useCounter increments count', () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
Mocking and Spying
Mock modules:
// Mock entire module
jest.mock('./api', () => ({
fetchUser: jest.fn(() => Promise.resolve({ id: 1, name: 'Test User' }))
}));
Mock functions:
const mockCallback = jest.fn();
mockCallback.mockReturnValue(42);
mockCallback.mockResolvedValue('async result');
// Check calls
expect(mockCallback).toHaveBeenCalledWith('expected', 'args');
expect(mockCallback).toHaveBeenCalledTimes(2);
Snapshot Testing
Snapshot tests capture component output and compare it on subsequent runs:
import { render } from '@testing-library/react';
import Card from './Card';
test('Card matches snapshot', () => {
const { container } = render(
<Card title="Test" description="Description" />
);
expect(container.firstChild).toMatchSnapshot();
});
β οΈ Warning: Use snapshots sparingly. They're brittle and don't validate functionalityβonly that output hasn't changed.
Examples with Explanations π―
Example 1: Type-Safe Form Component
Let's build a registration form with TypeScript and test it:
// RegistrationForm.tsx
import React, { useState, FormEvent } from 'react';
interface FormData {
username: string;
email: string;
password: string;
}
interface RegistrationFormProps {
onSubmit: (data: FormData) => void;
}
const RegistrationForm: React.FC<RegistrationFormProps> = ({ onSubmit }) => {
const [formData, setFormData] = useState<FormData>({
username: '',
email: '',
password: ''
});
const [errors, setErrors] = useState<Partial<FormData>>({});
const validate = (): boolean => {
const newErrors: Partial<FormData> = {};
if (formData.username.length < 3) {
newErrors.username = 'Username must be at least 3 characters';
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = 'Invalid email address';
}
if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (validate()) {
onSubmit(formData);
}
};
const handleChange = (field: keyof FormData) => (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, [field]: e.target.value });
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">Username</label>
<input
id="username"
type="text"
value={formData.username}
onChange={handleChange('username')}
/>
{errors.username && <span role="alert">{errors.username}</span>}
</div>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={formData.email}
onChange={handleChange('email')}
/>
{errors.email && <span role="alert">{errors.email}</span>}
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={formData.password}
onChange={handleChange('password')}
/>
{errors.password && <span role="alert">{errors.password}</span>}
</div>
<button type="submit">Register</button>
</form>
);
};
export default RegistrationForm;
Test file:
// RegistrationForm.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import RegistrationForm from './RegistrationForm';
describe('RegistrationForm', () => {
it('shows validation errors for invalid input', async () => {
const handleSubmit = jest.fn();
render(<RegistrationForm onSubmit={handleSubmit} />);
// Try to submit empty form
fireEvent.click(screen.getByRole('button', { name: /register/i }));
// Check for error messages
expect(await screen.findByText(/username must be at least 3 characters/i)).toBeInTheDocument();
expect(screen.getByText(/invalid email address/i)).toBeInTheDocument();
expect(screen.getByText(/password must be at least 8 characters/i)).toBeInTheDocument();
// onSubmit should not be called
expect(handleSubmit).not.toHaveBeenCalled();
});
it('submits form with valid data', async () => {
const handleSubmit = jest.fn();
render(<RegistrationForm onSubmit={handleSubmit} />);
// Fill in form
await userEvent.type(screen.getByLabelText(/username/i), 'johndoe');
await userEvent.type(screen.getByLabelText(/email/i), 'john@example.com');
await userEvent.type(screen.getByLabelText(/password/i), 'securePassword123');
// Submit
fireEvent.click(screen.getByRole('button', { name: /register/i }));
// Verify submission
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith({
username: 'johndoe',
email: 'john@example.com',
password: 'securePassword123'
});
});
});
});
Why this works:
- TypeScript ensures
formDatastructure matchesFormDatainterface - Type-safe event handlers:
handleChangereturns a properly-typed event handler - Tests verify both validation and successful submission
- Using
userEventsimulates real user typing (better thanfireEvent)
Example 2: Testing Component with API Call
// UserList.tsx
import React, { useState, useEffect } from 'react';
interface User {
id: number;
name: string;
email: string;
}
const UserList: React.FC = () => {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch('/api/users')
.then(res => {
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
})
.then((data: User[]) => {
setUsers(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []);
if (loading) return <div>Loading users...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
);
};
export default UserList;
Test file:
// UserList.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import UserList from './UserList';
const mockUsers = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
];
describe('UserList', () => {
beforeEach(() => {
global.fetch = jest.fn();
});
afterEach(() => {
jest.restoreAllMocks();
});
it('displays loading state initially', () => {
(global.fetch as jest.Mock).mockImplementation(() => new Promise(() => {}));
render(<UserList />);
expect(screen.getByText(/loading users/i)).toBeInTheDocument();
});
it('displays users after successful fetch', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => mockUsers
});
render(<UserList />);
await waitFor(() => {
expect(screen.getByText('Alice (alice@example.com)')).toBeInTheDocument();
expect(screen.getByText('Bob (bob@example.com)')).toBeInTheDocument();
});
});
it('displays error message on fetch failure', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: false
});
render(<UserList />);
expect(await screen.findByText(/error: failed to fetch/i)).toBeInTheDocument();
});
});
Key testing patterns:
- Mock
fetchglobally before each test - Test all states: loading, success, error
- Use
waitForto handle async state updates - Clean up mocks with
afterEach
Example 3: Testing Context and Provider
// ThemeContext.tsx
import React, { createContext, useContext, useState, ReactNode } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<Theme>('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
};
// ThemedButton.tsx
import React from 'react';
import { useTheme } from './ThemeContext';
const ThemedButton: React.FC = () => {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}
>
Current theme: {theme}
</button>
);
};
export default ThemedButton;
Test file:
// ThemedButton.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { ThemeProvider } from './ThemeContext';
import ThemedButton from './ThemedButton';
const renderWithTheme = (component: React.ReactElement) => {
return render(
<ThemeProvider>
{component}
</ThemeProvider>
);
};
describe('ThemedButton', () => {
it('starts with light theme', () => {
renderWithTheme(<ThemedButton />);
expect(screen.getByText(/current theme: light/i)).toBeInTheDocument();
});
it('toggles theme when clicked', () => {
renderWithTheme(<ThemedButton />);
const button = screen.getByRole('button');
// Initially light
expect(button).toHaveTextContent('light');
// Click to toggle
fireEvent.click(button);
expect(button).toHaveTextContent('dark');
// Click again
fireEvent.click(button);
expect(button).toHaveTextContent('light');
});
it('throws error when used outside provider', () => {
// Suppress console.error for this test
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
expect(() => render(<ThemedButton />)).toThrow('useTheme must be used within ThemeProvider');
spy.mockRestore();
});
});
Testing context tips:
- Create a custom render function that wraps components in providers
- Test context behavior through components that consume it
- Verify error handling when context is used incorrectly
Example 4: Integration Testing
// TodoApp.tsx (integration of multiple components)
import React, { useState } from 'react';
import TodoInput from './TodoInput';
import TodoList from './TodoList';
interface Todo {
id: number;
text: string;
completed: boolean;
}
const TodoApp: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const addTodo = (text: string) => {
const newTodo: Todo = {
id: Date.now(),
text,
completed: false
};
setTodos([...todos, newTodo]);
};
const toggleTodo = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div>
<h1>Todo App</h1>
<TodoInput onAdd={addTodo} />
<TodoList todos={todos} onToggle={toggleTodo} onDelete={deleteTodo} />
</div>
);
};
export default TodoApp;
Integration test:
// TodoApp.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TodoApp from './TodoApp';
describe('TodoApp Integration', () => {
it('completes full todo workflow', async () => {
render(<TodoApp />);
// Add first todo
const input = screen.getByPlaceholderText(/add a todo/i);
await userEvent.type(input, 'Buy groceries');
fireEvent.click(screen.getByRole('button', { name: /add/i }));
// Verify it appears
expect(screen.getByText('Buy groceries')).toBeInTheDocument();
// Add second todo
await userEvent.type(input, 'Walk the dog');
fireEvent.click(screen.getByRole('button', { name: /add/i }));
// Both should be visible
expect(screen.getByText('Buy groceries')).toBeInTheDocument();
expect(screen.getByText('Walk the dog')).toBeInTheDocument();
// Toggle first todo
const checkboxes = screen.getAllByRole('checkbox');
fireEvent.click(checkboxes[0]);
// Verify it's marked complete
expect(checkboxes[0]).toBeChecked();
// Delete second todo
const deleteButtons = screen.getAllByRole('button', { name: /delete/i });
fireEvent.click(deleteButtons[1]);
// Verify only first todo remains
expect(screen.getByText('Buy groceries')).toBeInTheDocument();
expect(screen.queryByText('Walk the dog')).not.toBeInTheDocument();
});
});
Common Mistakes β οΈ
TypeScript Mistakes
1. Using any type excessively
β Wrong:
const handleData = (data: any) => {
console.log(data.name); // No type safety
};
β Right:
interface UserData {
name: string;
age: number;
}
const handleData = (data: UserData) => {
console.log(data.name); // Type-safe
};
2. Not typing event handlers
β Wrong:
const handleChange = (e) => { // Implicit any
setValue(e.target.value);
};
β Right:
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
3. Forgetting optional chaining with refs
β Wrong:
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current.focus(); // Runtime error if null
β Right:
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus(); // Safe
Testing Mistakes
4. Testing implementation details
β Wrong:
it('sets state correctly', () => {
const { result } = renderHook(() => useState(0));
// Testing internal state management
});
β Right:
it('displays updated count when button clicked', () => {
render(<Counter />);
fireEvent.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
5. Not waiting for async updates
β Wrong:
it('displays user data', () => {
render(<UserProfile userId={1} />);
expect(screen.getByText('John Doe')).toBeInTheDocument(); // Fails: data not loaded yet
});
β Right:
it('displays user data', async () => {
render(<UserProfile userId={1} />);
expect(await screen.findByText('John Doe')).toBeInTheDocument();
});
6. Using wrong query methods
β Wrong:
expect(screen.queryByText('Error')).toBeInTheDocument(); // queryBy doesn't throw, use getBy
β Right:
expect(screen.getByText('Error')).toBeInTheDocument();
// Or for non-existence:
expect(screen.queryByText('Error')).not.toBeInTheDocument();
7. Not cleaning up side effects
β Wrong:
beforeEach(() => {
global.fetch = jest.fn();
});
// No cleanup - mocks leak between tests
β Right:
beforeEach(() => {
global.fetch = jest.fn();
});
afterEach(() => {
jest.restoreAllMocks();
});
8. Over-relying on snapshots
β Wrong:
it('renders correctly', () => {
const { container } = render(<ComplexComponent />);
expect(container).toMatchSnapshot(); // Brittle, doesn't verify behavior
});
β Right:
it('displays user greeting', () => {
render(<ComplexComponent user={{ name: 'Alice' }} />);
expect(screen.getByText('Hello, Alice')).toBeInTheDocument();
});
π€ Did you know? TypeScript was created by Microsoft in 2012, and React officially adopted TypeScript types in 2016. Today, over 60% of React projects use TypeScript!
Key Takeaways π―
β TypeScript provides type safety and better DX - catches bugs at compile time, enables autocomplete
β Type your props, state, and events - use interfaces for component props, type hooks explicitly when needed
β Test user behavior, not implementation - focus on what users see and do, not internal state or methods
β
Use React Testing Library queries wisely - prefer accessible queries like getByRole and getByLabelText
β
Handle async properly in tests - use waitFor, findBy, or mock promises correctly
β Mock external dependencies - isolate components by mocking API calls, context, or third-party libraries
β Follow the testing pyramid - many unit tests, some integration tests, few end-to-end tests
β
Start strict, stay strict - enable "strict": true in TypeScript from the beginning
π Quick Reference Card
| Concept | Key Points |
|---|---|
| TypeScript Setup | npx create-react-app my-app --template typescript |
| Typing Props | interface MyProps { name: string; } |
| useState with TS | useState<Type | null>(null) |
| useRef with TS | useRef<HTMLElement>(null) |
| Event Types | React.ChangeEvent<HTMLInputElement> |
| Testing Library | @testing-library/react |
| Query Priority | getByRole β getByLabelText β getByText |
| Async Testing | await screen.findByText() or waitFor() |
| Mock Functions | jest.fn(), mockReturnValue() |
| AAA Pattern | Arrange β Act β Assert |
π Further Study
- TypeScript Official Docs: https://www.typescriptlang.org/docs/handbook/react.html
- React Testing Library Documentation: https://testing-library.com/docs/react-testing-library/intro/
- Jest Documentation: https://jestjs.io/docs/getting-started
Congratulations! You now have the foundational knowledge to write type-safe React components and test them thoroughly. Practice by converting an existing project to TypeScript and adding comprehensive tests. Remember: TypeScript and testing are investments that pay off exponentially as your codebase grows! π