C
JavaScript/TypeScript/Lesson 21

TypeScript 최소 기초 — 타입 에러 읽기 · any 회피

45분·theory

TypeScript 최소 기초 — 타입 에러 읽기 · any 회피

🎯 이 lesson 을 읽고 나면

이 lesson 을 다 읽고 나면 아래 3가지를 자신 있게 할 수 있습니다.

  • ✅ any 를 unknown 으로 바꿔 안전성 회복
  • ✅ interface vs type · 유니언 · 옵셔널 · never
  • ✅ Utility Types 5개 (Partial · Pick · Omit · Record · Required)

학습 목표를 체크리스트로 두고 다 답할 수 있게 되면 lesson 을 닫으세요.

⚠️ tsconfig 전제 조건 — strict 모드 기준

모든 예시는 "strict": true 기준

jsonc
{
  "compilerOptions": {
    "strict": true,            // 아래 7가지 한꺼번에 켬
    // strictNullChecks: null/undefined 별도 타입
    // noImplicitAny: 추론 실패 시 any 자동 부여 금지
    // strictFunctionTypes
    // strictBindCallApply
    // strictPropertyInitialization
    // noImplicitThis
    // useUnknownInCatchVariables
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler"
  }
}

strict 가 꺼져 있으면 — 다른 언어가 됩니다

  • null모든 타입에 할당 가능string 변수에 null 들어가도 에러 X
  • noImplicitAny 꺼지면 function f(x)x 가 자동 any → 타입 검사 무력화
  • unknown / never 의 narrowing 동작이 느슨해짐

확인 방법

bash
npx tsc --showConfig | grep strict

규칙: 신규 프로젝트는 strict: true 기본값 (Vite·Next.js·CRA 모두). 기존 프로젝트에서 실습 전 반드시 확인하세요. 꺼져있으면 "왜 책 대로 안 되지?" 의 99% 원인이 이것.

왜 TypeScript 가 토큰을 절약하나

사실 — 2026년 신규 프로젝트의 90% 는 TypeScript

Cursor, v0.dev, Claude Code, GitHub Copilot — 모두 TypeScript 를 우선 생성 합니다. JS 로 시작해도 어느 순간 .ts 파일 이 추가됩니다.

토큰 절약 메커니즘 — 타입 정보가 컨텍스트

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

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

AI 에게 "sendEmail(currentUser, welcome) 호출 코드 짜줘" 라고 하면:

  • JS 라면 → user 가 뭔지·어떤 필드가 있는지 다시 알려줘야 함
  • TS 라면 → AI 가 interface 만 보고 자동 추론. 추가 설명 불필요

타입 정의 자체가 AI 컨텍스트. 추가 프롬프트 = 추가 토큰. TS 로 줄어듭니다.

기본 타입 8가지

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];     // 튜플
let anyVal: any = '아무거나';               // ❌ 사용 자제
let unknownVal: unknown = JSON.parse(raw); // ✅ any 대신
let voidVal: void = undefined;             // 리턴 없음

any 가 왜 위험한가

typescript
const x: any = 'hello';
x.toFixed(2);   // 런타임 에러 — 컴파일러가 검사 안 함

any 는 TS 의 모든 안전성을 끔. AI 가 any 를 쓰면 unknown 으로 바꿔달라 요청하세요.

unknown — 안전한 any

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

// ❌ 그냥 쓰면 컴파일 에러 — "unknown 인데 어떻게 toUpperCase 부르려고?"
// x.toUpperCase();   // TS error: 'x' is of type 'unknown'

// ✅ 먼저 타입 확인 (narrowing) 후 사용
if (typeof x === 'string') {
    console.log(x.toUpperCase());   // "HELLO"   ← 이 블록 안에선 x: string 으로 좁혀짐
}

// 💡 any: 모든 검사 끔 → 위험
//    unknown: 검사 전엔 못 씀 → 안전

사용 전 타입 확인 강제. API 응답 파싱·외부 입력에 표준.

interface vs type · 유니언 · 옵셔널

interface vs type — 언제 뭘 쓰나

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

type Status = 'active' | 'inactive';
type Point = { x: number; y: number };
interfacetype
객체 형태
유니언/인터섹션
extends✅ (&)
선언 병합

규칙: 객체 모양은 interface, 그 외 (유니언·튜플·매핑) 는 type. 팀 컨벤션이 "무조건 type" 인 경우도 많음.

유니언 |여러 타입 중 하나

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

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

리터럴 유니언 이 가장 자주:

typescript
type Role = 'admin' | 'user' | 'guest';
// 'admin', 'user', 'guest' 셋 중 하나만 허용

enum 대신 리터럴 유니언 이 현재 표준.

옵셔널 ?

typescript
interface CreateUserInput {
    email: string;
    name: string;
    age?: number;       // 있을 수도 없을 수도
    role?: 'admin' | 'user';
}

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

? 가 붙으면 undefined 도 허용. 함수 파라미터·DTO 에 자주.

제네릭 <T> — 기본 형태

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     ← 타입: number | undefined
console.log(b);   // 'a'   ← 타입: string | undefined

// 💡 호출할 때마다 T 가 "인자 타입" 으로 자동 결정됨
//    → 함수 하나로 어떤 배열 타입이든 안전하게 처리

"타입을 변수처럼" — 호출할 때 결정. 라이브러리 (React Hook, Array 메서드) 가 광범위 사용.

never — 절대 도달 불가 타입

never 의 정체

모든 타입의 하위 타입. "존재할 수 없는 값" 을 표현. 함수가 절대 정상 반환 못 함 (throw 또는 무한 루프) 도 never:

typescript
function fail(msg: string): never {
    throw new Error(msg);   // 정상 반환 없음
}

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

Exhaustive Check — switch 누락을 컴파일 타임에 잡기

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:
            // 위 case 가 Shape 의 모든 멤버를 처리하면 s 의 타입이 never
            const _check: never = s;
            return _check;
    }
}

Shape'star' 를 추가하면 — switch 가 그것을 처리 안 하므로 s 의 타입이 'star' 로 좁혀짐 → never 변수에 할당 불가 → 컴파일 에러. "새 케이스 추가했으면 여기도 처리해" 라는 컴파일러의 강제 알림.

Java 의 sealed class, Rust 의 enum 매칭 과 같은 사상의 TS 버전.

never vs void — 면접 단골

voidnever
의미반환값 없음절대 반환 안 함
함수 종료정상 종료 (return undefined)throw / 무한 루프
변수 할당let x: void = undefined 가능어떤 값도 할당 불가
typescript
function logOnly(msg: string): void { console.log(msg); }      // 정상 종료
function fail(msg: string): never  { throw new Error(msg); }    // throw

const a: void = undefined;     // OK
const b: never = ???;          // 어떤 값을 넣어도 컴파일 에러

실무 활용 — discriminated union 의 안전망

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;   // 새 event 추가 시 여기서 잡힘
            throw new Error('unhandled event');
    }
}

이 패턴이 TS 코드에 적힌 모든 곳 — discriminated union + never default 체크. 이걸 알면 면접에서 "TS 가 JS 보다 좋은 점" 답이 단단해집니다.

Utility Types — 면접 단골 5개

TS 내장 타입 변환기

기존 타입을 가공해 새 타입 만들기. 5개만 알면 실무 99% 커버.

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

1. Partial<T> — 모든 필드 optional

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

// PATCH API 의 표준 — 일부 필드만 보내는 요청 body
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> — 모든 필드 필수 (Partial 의 반대)

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

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

// 게시 전 검증 게이트
function publish(d: Required<Draft>) { /* title·body 모두 있다고 보장 */ }

3. Pick<T, K> — 일부 필드만 선택

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> — 일부 필드 제외

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

// 민감 정보 제거 (API 응답에서 비밀번호·이메일 제거) 표준 패턴
function toPublic(u: User): PublicUser {
    const { email, role, ...pub } = u;
    return pub;
}

5. Record<K, V> — 키-값 맵

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']
};

enum 의 대체 — 키가 유니언 타입이면 컴파일러가 모든 키를 채웠는지 검사. 누락하면 에러.

면접 포인트 — 이 3개 패턴이 실무 매일 등장

  • Partial → PATCH API DTO (Partial<User>)
  • Omit → 민감 필드 제거 (Omit<User, 'password' | 'tokenSecret'>)
  • Record → 권한·언어·상태 맵 (Record<Role, Permission[]>)

다른 Utility Types — 알면 도움

typescript
type A = ReturnType<typeof fn>;       // 함수의 반환 타입 추출
type B = Parameters<typeof fn>;       // 함수의 파라미터 튜플
type C = Awaited<Promise<string>>;    // Promise unwrap → string
type D = NonNullable<string | null>;  // null/undefined 제거
type E = Readonly<User>;              // 모든 필드 readonly

ReturnType / Awaited 도 면접 자주 등장. "이 함수의 리턴 타입을 어떻게 다른 곳에서 재사용해요?" 답이 ReturnType.

타입 에러 읽기 · 타입 단언 · React 와의 만남

에러 메시지를 천천히 읽으세요

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

아래쪽에 진짜 원인. "email 이 빠졌다" — 주는 쪽이 받는 쪽 타입을 만족 못함.

타입 단언 as너무 자주 쓰면 위험

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

"믿어, 이건 HTMLDivElement 야" — TS 의 검사를 수동으로 무시.

남용 금지. as 가 많은 코드 = TS 안 쓰는 것과 동일. narrowing (if (typeof x === ...)) 이나 타입 가드 가 우선.

타입 가드

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 완료
}

x is User반환 타입 — 호출 위치에서 TS 가 narrowing 적용.

React 컴포넌트 타입

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>;
}

props 인터페이스를 분리해 정의 가 표준.

useState 타입 추론

tsx
const [count, setCount] = useState(0);             // number 추론
const [user, setUser] = useState<User | null>(null);  // 명시

null 가능성이 있으면 명시 — 추론이 null 만 보고 null 타입 으로 좁혀버리는 함정.

API 응답 타입

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();   // Promise<any> 를 Promise<User> 로 단언
}

리턴 타입을 명시 하면 호출자에서 자동 narrowing.

🤖 AI 에게 이렇게 요청해보세요

  • "이 함수에 TypeScript 타입을 붙여줘. any 는 쓰지 말고 unknown 또는 명시 타입으로"
  • "이 에러 메시지를 한글로 해석해줘" (그대로 붙여 보내면 AI 가 풀이)
  • "이 as 단언을 타입 가드로 바꿔줘"

TS 5가지 (interface · 유니언 · 옵셔널 · narrowing · 제네릭) 만 알면 바이브 코딩의 90% 가 풀립니다.

`as` vs `as const` — 둘은 완전히 다른 도구

as — 타입 시스템 우회 (위험)

typescript
const role = 'admin' as 'admin';   // 억지 단언
const x = JSON.parse(raw) as User;  // "믿어, 이건 User 야"

컴파일러의 검사를 수동으로 끔. 잘못 단언하면 런타임에서 폭발.

typescript
// ❌ 위험한 as
const data = '{ malformed }' as User;
data.email.toLowerCase();   // 런타임 TypeError

as const — 리터럴 타입 고정 (안전한 대안)

typescript
const ROLES = ['admin', 'user', 'guest'] as const;
// 타입: readonly ['admin', 'user', 'guest']  (좁아진 리터럴)

// 만약 as const 없었으면:
const ROLES2 = ['admin', 'user', 'guest'];
// 타입: string[]   ("이 배열은 string 들의 배열"  — 정보 손실)

배열 → 유니언 타입 생성 패턴 — as const 의 진짜 가치

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

값과 타입이 항상 동기화. ROLES'staff' 추가하면 Role 도 자동으로 'admin' | 'user' | 'guest' | 'staff' 가 됨. 일반 enum 의 한계를 우아하게 극복.

객체 freezing — 설정·상수 모음

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

// CONFIG.timeout 의 타입: 3000 (number 아님 — 정확히 그 리터럴)
// CONFIG.endpoint 의 타입: 'https://api.example.com'
// CONFIG 의 모든 속성 readonly

CONFIG.timeout = 5000;   // ❌ 컴파일 에러 — readonly

실무 예시 — 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 */ }
}

한 줄 요약

  • as컴파일러를 속여서 타입을 강제. 거의 쓰지 마세요.
  • as const값에서 정확한 리터럴 타입을 뽑아냄. 안전. 자주 쓰세요.

⚡ 직접 해보기 — TS 개념 (런타임 타입 검증)

sandbox 가 TS 컴파일러 미지원이라 *컴파일 타임 검사 대신 런타임 typeof / instanceof 로* TypeScript 의 핵심 개념 (unknown · narrowing · 타입 가드) 시연.
✏️ JS 코드
📟 콘솔 출력
▶ 실행 버튼을 눌러보세요
⚠️ 브라우저 샌드박스에서 실행 — console.log()만 지원, alert/fetch 불가
TypeScript 최소 기초 — 타입 에러 읽기 · any 회피 - JavaScript