C
JavaScript/TypeScript/Lesson 21

TypeScript Essentials — Reading Type Errors & Avoiding any

45 min·theory

TypeScript Essentials — Reading Type Errors & Avoiding any

🎯 After reading this lesson

By the end of this lesson, you will be able to confidently do the following three things.

  • ✅ Replace any with unknown to restore type safety
  • ✅ Understand interface vs type, unions, optionals, and never
  • ✅ Use 5 Utility Types (Partial, Pick, Omit, Record, Required)

Keep these learning goals as a checklist — close the lesson once you can answer all of them.

⚠️ tsconfig prerequisite — based on strict mode

All examples assume "strict": true

jsonc
{
  "compilerOptions": {
    "strict": true,            // enables all 7 options below at once
    // strictNullChecks: null/undefined are separate types
    // noImplicitAny: prevents implicit any when inference fails
    // strictFunctionTypes
    // strictBindCallApply
    // strictPropertyInitialization
    // noImplicitThis
    // useUnknownInCatchVariables
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler"
  }
}

When strict is off — it becomes a different language

  • null is assignable to every type → no error even if null goes into a string variable
  • With noImplicitAny off, the x in function f(x) becomes implicit any → type checking is disabled
  • Narrowing behavior for unknown / never becomes looser

How to verify

bash
npx tsc --showConfig | grep strict

Rule: New projects default to strict: true (Vite, Next.js, CRA — all of them). Always check before practicing on an existing project. If it's off, that's the cause of 99% of "why doesn't it work like in the book?" moments.

Why TypeScript saves tokens

Fact — 90% of new projects in 2026 use TypeScript

Cursor, v0.dev, Claude Code, GitHub Copilot — all generate TypeScript by default. Even if you start with JS, .ts files will be added sooner or later.

The token-saving mechanism — type information as context

typescript
interface User {
    id: number;
    email: string;
    role: 'admin' | 'user';
}

function sendEmail(user: User, template: string): Promise<void> { ... }

When you ask AI to "write the code to call sendEmail(currentUser, welcome)":

  • In JS → you have to re-explain what user is and what fields it has
  • In TS → the AI infers everything from the interface alone. No extra explanation needed.

The type definition itself is the AI context. Extra prompting = extra tokens. TypeScript reduces both.

The 8 basic types

typescript
let n: number = 42;
let s: string = 'hello';
let b: boolean = true;
let a: number[] = [1, 2, 3];
let tup: [string, number] = ['A', 30];     // tuple
let anyVal: any = 'anything';               // ❌ avoid using
let unknownVal: unknown = JSON.parse(raw); // ✅ use instead of any
let voidVal: void = undefined;             // no return value

Why any is dangerous

typescript
const x: any = 'hello';
x.toFixed(2);   // runtime error — the compiler doesn't check this

any disables all of TypeScript's safety. When AI writes any, ask it to replace it with unknown.

unknown — the safe any

typescript
const raw = '"hello"';
const x: unknown = JSON.parse(raw);

// ❌ Using it directly causes a compile error — "x is unknown, how can you call toUpperCase?"
// x.toUpperCase();   // TS error: 'x' is of type 'unknown'

// ✅ Check the type first (narrowing), then use it
if (typeof x === 'string') {
    console.log(x.toUpperCase());   // "HELLO"   ← inside this block, x is narrowed to string
}

// 💡 any: disables all checks → dangerous
//    unknown: cannot be used before checking → safe

Forces type verification before use. The standard for API response parsing and external input.

interface vs type, unions, and optionals

interface vs type — when to use which

typescript
interface User {
    id: number;
    name: string;
}

type Status = 'active' | 'inactive';
type Point = { x: number; y: number };
interfacetype
Object shape
Union/Intersection
extends✅ (&)
Declaration merging

Rule: Use interface for object shapes, use type for everything else (unions, tuples, mappings). Many teams also follow a "type for everything" convention.

Union |one of several types

typescript
type ID = number | string;
function find(id: ID) { ... }

find(123);       // OK
find('abc');     // OK
find(true);      // ❌

Literal unions are most common:

typescript
type Role = 'admin' | 'user' | 'guest';
// Only one of 'admin', 'user', or 'guest' is allowed

Literal unions are now the standard instead of enums.

Optional ?

typescript
interface CreateUserInput {
    email: string;
    name: string;
    age?: number;       // may or may not be present
    role?: 'admin' | 'user';
}

function create(input: CreateUserInput) {
    const age = input.age ?? 0;
    // ...
}

A ? means undefined is also allowed. Common in function parameters and DTOs.

Generic <T> — basic form

typescript
function firstItem<T>(arr: T[]): T | undefined {
    return arr[0];
}

const a = firstItem([1, 2, 3]);     // T = number → number | undefined
const b = firstItem(['a', 'b']);    // T = string → string | undefined

console.log(a);   // 1     ← type: number | undefined
console.log(b);   // 'a'   ← type: string | undefined

// 💡 T is automatically determined by the argument type at each call
//    → one function handles any array type safely

"Type as a variable" — determined at call time. Widely used in libraries (React Hooks, Array methods).

never — the unreachable type

What never is

The bottom type — a subtype of every type. Represents "a value that cannot exist." A function that can never return normally (throws or runs forever) also returns never:

typescript
function fail(msg: string): never {
    throw new Error(msg);   // no normal return
}

function loop(): never {
    while (true) { /* forever */ }
}

Exhaustive Check — catch missing switch cases at compile time

typescript
type Shape = 'circle' | 'square' | 'triangle';

function area(s: Shape): number {
    switch (s) {
        case 'circle':   return Math.PI;
        case 'square':   return 1;
        case 'triangle': return 0.5;
        default:
            // If all members of Shape are handled above, s is narrowed to never
            const _check: never = s;
            return _check;
    }
}

If you add 'star' to Shape — since the switch doesn't handle it, s is narrowed to 'star' → cannot be assigned to a never variable → compile error. The compiler forces you to handle the new case.

The TypeScript equivalent of Java's sealed classes and Rust's exhaustive enum matching.

never vs void — a classic interview question

voidnever
MeaningNo return valueNever returns
Function endNormal (return undefined)throw / infinite loop
Variable assignmentlet x: void = undefined is OKNo value can be assigned
typescript
function logOnly(msg: string): void { console.log(msg); }      // normal return
function fail(msg: string): never  { throw new Error(msg); }    // throws

const a: void = undefined;     // OK
const b: never = ???;          // any value causes a compile error

Real-world usage — the safety net for discriminated unions

typescript
type Event =
    | { kind: 'click'; x: number; y: number }
    | { kind: 'scroll'; offset: number }
    | { kind: 'keypress'; key: string };

function handle(e: Event): string {
    switch (e.kind) {
        case 'click':    return `clicked at ${e.x},${e.y}`;
        case 'scroll':   return `scrolled ${e.offset}`;
        case 'keypress': return `pressed ${e.key}`;
        default:
            const _: never = e;   // caught here when a new event is added
            throw new Error('unhandled event');
    }
}

Whenever you see this pattern in TypeScript code — discriminated union + never default check. Knowing this gives you a solid answer to "what makes TypeScript better than JavaScript?" in interviews.

Utility Types — 5 interview essentials

TypeScript's built-in type transformers

Create new types by transforming existing ones. These 5 cover 99% of real-world use cases.

typescript
interface User {
    id: number;
    name: string;
    email: string;
    role: 'admin' | 'user';
}

1. Partial<T> — make all fields optional

typescript
type UpdateUser = Partial<User>;
// Result: { id?: number; name?: string; email?: string; role?: ... }

// The standard for PATCH API request bodies — send only some fields
function patchUser(id: number, patch: Partial<User>) {
    return fetch(`/api/users/${id}`, {
        method: 'PATCH',
        body: JSON.stringify(patch)
    });
}

patchUser(1, { name: 'Alice' });          // OK
patchUser(1, { name: 'A', email: 'a' });  // OK
patchUser(1, {});                          // OK

2. Required<T> — make all fields required (opposite of Partial)

typescript
interface Draft { title?: string; body?: string; }

type Published = Required<Draft>;
// { title: string; body: string }

// A validation gate before publishing
function publish(d: Required<Draft>) { /* title and body are both guaranteed */ }

3. Pick<T, K> — select only some fields

typescript
type UserCard = Pick<User, 'id' | 'name'>;
// { id: number; name: string }

function renderCard(u: UserCard) {
    return `<div>${u.id}: ${u.name}</div>`;
}

4. Omit<T, K> — exclude some fields

typescript
type PublicUser = Omit<User, 'email' | 'role'>;
// { id: number; name: string }

// Standard pattern for removing sensitive data (passwords, emails from API responses)
function toPublic(u: User): PublicUser {
    const { email, role, ...pub } = u;
    return pub;
}

5. Record<K, V> — key-value map

typescript
type RolePermissions = Record<'admin' | 'user' | 'guest', string[]>;
// { admin: string[]; user: string[]; guest: string[] }

const perms: RolePermissions = {
    admin: ['read', 'write', 'delete'],
    user:  ['read', 'write'],
    guest: ['read']
};

An alternative to enums — when the key is a union type, the compiler checks that all keys are present. Missing one causes an error.

Interview talking points — these 3 patterns appear in production every day

  • Partial → PATCH API DTO (Partial<User>)
  • Omit → remove sensitive fields (Omit<User, 'password' | 'tokenSecret'>)
  • Record → permission/locale/state maps (Record<Role, Permission[]>)

Other Utility Types — useful to know

typescript
type A = ReturnType<typeof fn>;       // extract the return type of a function
type B = Parameters<typeof fn>;       // parameter tuple of a function
type C = Awaited<Promise<string>>;    // unwrap a Promise → string
type D = NonNullable<string | null>;  // remove null/undefined
type E = Readonly<User>;              // make all fields readonly

ReturnType / Awaited also come up in interviews. The answer to "how do you reuse a function's return type elsewhere?" is ReturnType.

Reading type errors, type assertions, and working with React

Read error messages slowly

code
Type '{ id: number; }' is not assignable to type 'User'.
  Property 'email' is missing in type '{ id: number; }' but required in type 'User'.

The real cause is at the bottom. "'email' is missing" — the provider doesn't satisfy the receiver's type.

Type assertion asdangerous if overused

typescript
const el = document.getElementById('app') as HTMLDivElement;

"Trust me, this is an HTMLDivElement"manually bypassing TypeScript's checks.

Do not overuse. Code full of as is no different from not using TypeScript at all. Narrowing (if (typeof x === ...)) or type guards should come first.

Type guards

typescript
function isUser(x: unknown): x is User {
    return typeof x === 'object' && x !== null && 'email' in x;
}

if (isUser(data)) {
    console.log(data.email);   // ✅ narrowing complete
}

x is User is the return type — TypeScript applies narrowing at the call site.

React component types

tsx
interface ButtonProps {
    label: string;
    onClick: () => void;
    variant?: 'primary' | 'secondary';
}

function Button({ label, onClick, variant = 'primary' }: ButtonProps) {
    return <button onClick={onClick} className={variant}>{label}</button>;
}

Defining a separate props interface is the standard approach.

useState type inference

tsx
const [count, setCount] = useState(0);             // inferred as number
const [user, setUser] = useState<User | null>(null);  // explicit

Be explicit when null is possible — the inference may narrow to just null type and cause problems.

API response types

typescript
async function fetchUser(id: number): Promise<User> {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error('failed');
    return res.json();   // assert Promise<any> as Promise<User>
}

Specifying the return type explicitly enables automatic narrowing at the call site.

🤖 Try asking AI like this

  • "Add TypeScript types to this function. Don't use any — use unknown or explicit types."
  • "Explain this error message in plain English" (just paste it and the AI will walk you through it)
  • "Replace this as assertion with a type guard"

Knowing just 5 things (interface, unions, optionals, narrowing, generics) will solve 90% of vibe coding issues.

`as` vs `as const` — two completely different tools

as — bypasses the type system (dangerous)

typescript
const role = 'admin' as 'admin';   // forced assertion
const x = JSON.parse(raw) as User;  // "trust me, this is User"

Manually turns off the compiler's checks. A wrong assertion blows up at runtime.

typescript
// ❌ dangerous as
const data = '{ malformed }' as User;
data.email.toLowerCase();   // runtime TypeError

as const — fixes literal types (safe alternative)

typescript
const ROLES = ['admin', 'user', 'guest'] as const;
// type: readonly ['admin', 'user', 'guest']  (narrowed literals)

// Without as const:
const ROLES2 = ['admin', 'user', 'guest'];
// type: string[]   ("an array of strings" — information lost)

Array → union type generation pattern — the real value of as const

typescript
const ROLES = ['admin', 'user', 'guest'] as const;
type Role = typeof ROLES[number];
// type Role = 'admin' | 'user' | 'guest'

Values and types are always in sync. Add 'staff' to ROLES and Role automatically becomes 'admin' | 'user' | 'guest' | 'staff'. An elegant solution to the limitations of plain enums.

Object freezing — config and constant collections

typescript
const CONFIG = {
    endpoint: 'https://api.example.com',
    timeout: 3000,
    retries: 3
} as const;

// type of CONFIG.timeout: 3000 (not number — exactly that literal)
// type of CONFIG.endpoint: 'https://api.example.com'
// all properties of CONFIG are readonly

CONFIG.timeout = 5000;   // ❌ compile error — readonly

Real-world example — creating a Discriminated Union

typescript
const EVENT_TYPES = ['click', 'scroll', 'keypress'] as const;
type EventType = typeof EVENT_TYPES[number];
// 'click' | 'scroll' | 'keypress'

interface AppEvent {
    type: EventType;
    timestamp: number;
}

function handle(e: AppEvent) {
    if (e.type === 'click') { /* narrow */ }
}

One-line summary

  • astricks the compiler into accepting a type. Avoid it almost always.
  • as constextracts exact literal types from values. Safe. Use it often.

⚡ Try it yourself — TS concepts (runtime type validation)

Since the sandbox doesn't support the TypeScript compiler, *instead of compile-time checking, this demo uses runtime `typeof` / `instanceof`* to illustrate core TypeScript concepts (unknown, narrowing, and type guards).
✏️ JS 코드
📟 Console output
▶ Press the Run button
⚠️ Runs in a browser sandbox — only console.log() is supported; alert/fetch are not.
TypeScript Essentials — Reading Type Errors & Avoiding any - JavaScript