The TypeScript debate is exhausting. One side claims it eliminates an entire class of bugs. The other side says it is just JavaScript with extra steps and build time. Both are wrong. TypeScript does not replace tests. It replaces guessing. It replaces the mental overhead of remembering what shape every object has at every point in your code.
TypeScript does not catch all bugs. It catches the boring ones so you can focus on the interesting ones.
The real value is in the developer experience. Autocomplete that actually works. Refactoring that does not break production. Documentation that stays in sync because the compiler enforces it. When you change a function signature, TypeScript tells you every call site that needs updating. That is not a luxury. That is productivity.
// Without types, this compiles and fails at runtime
function getUser(id) {
    return fetch(`/api/users/${id}`).then(r => r.json());
}

getUser('not-a-number'); // Runtime error, maybe hours later

// With types, the mistake is caught before you save
interface User {
    id: number;
    name: string;
    email: string;
    role: 'admin' | 'editor' | 'viewer';
}

async function getUser(id: number): Promise<User> {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) throw new Error('Failed to fetch user');
    return response.json();
}

getUser('not-a-number');
//     ^^^^^^^^^^^^^^^^
// Argument of type 'string' is not assignable to parameter of type 'number'.
Discriminated unions are where TypeScript shines. They let you model complex state machines with exhaustiveness checking. The compiler forces you to handle every case. No forgotten switch branches. No impossible states.
type AsyncState<T> =
    | { status: 'idle' }
    | { status: 'loading' }
    | { status: 'success'; data: T }
    | { status: 'error'; error: Error };

function renderUser(state: AsyncState<User>): string {
    switch (state.status) {
        case 'idle':
            return '<p>Click to load</p>';
        case 'loading':
            return '<p>Loading...</p>';
        case 'success':
            return `<p>Hello, ${state.data.name}</p>`;
        case 'error':
            return `<p>Error: ${state.error.message}</p>`;
        default:
            // Exhaustiveness check: if you add a new status,
            // this line throws a compile error
            const _exhaustive: never = state;
            return '';
    }
}
The build time complaint is real but overstated. Modern TypeScript with project references and incremental compilation is fast. And even if it were not, the time saved by catching errors at compile time far exceeds any build overhead. TypeScript is not about type safety as an abstract ideal. It is about shipping fewer bugs to production. That is worth a few extra seconds in CI.