포스트

TypeScript 5.x 고급 타입 패턴 완벽 가이드 - 실무에서 바로 쓰는 타입 안전성 전략

TypeScript 5.x의 새로운 기능부터 제네릭 고급 활용, 커스텀 유틸리티 타입, Branded Types, Exhaustive Check까지. 실무에서 타입 안전성을 높이는 모든 패턴을 예제와 함께 완벽 정리

TypeScript 5.x 고급 타입 패턴 완벽 가이드 - 실무에서 바로 쓰는 타입 안전성 전략

TypeScript 5.x 소개

TypeScript는 JavaScript에 정적 타입을 추가한 언어로, 대규모 애플리케이션 개발에서 필수적인 도구가 되었습니다. TypeScript 5.x 버전은 2023년 3월 5.0 출시를 시작으로 꾸준히 발전해왔으며, 각 버전마다 개발자 경험을 크게 향상시키는 기능들이 추가되었습니다.

TypeScript 5.0 ~ 5.6 주요 업데이트 히스토리

버전출시일핵심 기능
5.02023.03Decorators 정식 지원, const 타입 파라미터
5.12023.06암묵적 반환 타입 개선, JSX 타입 확장
5.22023.08using 선언, Decorator Metadata
5.32023.11Import Attributes, switch(true) Narrowing
5.42024.03NoInfer<T>, 클로저에서 Narrowed Types 보존
5.52024.06Inferred Type Predicates, 정규식 문법 체크
5.62024.09Iterator Helper Methods, Nullish/Truthy 체크 강화

5.3, 5.4, 5.5, 5.6의 핵심 기능 요약

TypeScript 5.3 (2023년 11월)

  • Import Attributes: 모듈 타입 명시적 지정 가능
  • resolution-mode: import 타입에서 해석 모드 지정
  • switch(true) narrowing: 패턴 매칭 스타일 타입 좁히기 지원

TypeScript 5.4 (2024년 3월)

  • NoInfer<T>: 제네릭 추론 제어를 위한 유틸리티 타입
  • 클로저에서 narrowed types 보존: 콜백 함수 내 타입 안전성 향상
  • Object.groupBy/Map.groupBy: 새로운 그룹화 메서드 타입 지원

TypeScript 5.5 (2024년 6월)

  • Inferred Type Predicates: 타입 가드 자동 추론
  • 정규식 문법 체크: 컴파일 타임에 정규식 오류 감지
  • 새로운 Set 메서드 타입: union(), intersection(), difference()

TypeScript 5.6 (2024년 9월)

  • Disallowed Nullish and Truthy Checks: 항상 참/거짓인 조건 검사 오류
  • Iterator Helper Methods: map(), filter() 등 이터레이터 헬퍼 지원
  • Arbitrary Module Identifiers: 모듈 식별자 유연성 향상

TypeScript의 발전 방향

TypeScript 팀은 다음 방향으로 언어를 발전시키고 있습니다:

  1. 타입 추론 강화: 개발자가 명시적으로 타입을 지정하지 않아도 정확한 타입 추론
  2. 개발자 경험 개선: 에디터 지원, 오류 메시지 개선, 컴파일 속도 향상
  3. ECMAScript 표준 추종: 새로운 JavaScript 기능의 빠른 지원
  4. 타입 안전성 강화: 런타임 오류를 컴파일 타임에 더 많이 잡아내기

TypeScript 5.x 새로운 기능들

5.3: Import Attributes

Import Attributes(이전 Import Assertions)는 모듈을 import할 때 추가 메타데이터를 제공하는 방법입니다.

1
2
3
4
5
6
7
8
// JSON 파일을 명시적으로 JSON 모듈로 import
import config from './config.json' with { type: 'json' };

// 타입만 import할 때도 사용 가능
import type { Config } from './config.json' with { type: 'json' };

// 동적 import에서도 사용
const data = await import('./data.json', { with: { type: 'json' } });

실제 사용 사례

1
2
3
4
5
6
7
// package.json에서 버전 정보 가져오기
import packageJson from '../package.json' with { type: 'json' };

console.log(`App Version: ${packageJson.version}`);

// CSS 모듈 (번들러 지원 필요)
import styles from './styles.css' with { type: 'css' };

5.3: resolution-mode in import types

import type에서 해석 모드를 명시적으로 지정할 수 있습니다.

1
2
3
4
5
// Node.js ESM 방식으로 타입 해석
import type { PackageJson } from 'pkg' with { 'resolution-mode': 'import' };

// CommonJS 방식으로 타입 해석
import type { Config } from 'config' with { 'resolution-mode': 'require' };

5.3: switch(true) narrowing

switch(true) 패턴에서 타입 narrowing이 정확하게 동작합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
interface Circle {
  kind: 'circle';
  radius: number;
}

interface Square {
  kind: 'square';
  sideLength: number;
}

interface Triangle {
  kind: 'triangle';
  base: number;
  height: number;
}

type Shape = Circle | Square | Triangle;

function getArea(shape: Shape): number {
  // switch(true) 패턴으로 복잡한 조건 처리
  switch (true) {
    case shape.kind === 'circle':
      // shape는 Circle로 narrowing됨
      return Math.PI * shape.radius ** 2;

    case shape.kind === 'square':
      // shape는 Square로 narrowing됨
      return shape.sideLength ** 2;

    case shape.kind === 'triangle':
      // shape는 Triangle로 narrowing됨
      return (shape.base * shape.height) / 2;

    default:
      // 컴파일러가 모든 케이스를 처리했음을 인식
      const _exhaustive: never = shape;
      throw new Error(`Unknown shape: ${_exhaustive}`);
  }
}

5.4: NoInfer 유틸리티 타입

NoInfer<T>는 제네릭 타입 추론을 제어하는 강력한 도구입니다. 특정 위치에서 타입 추론을 방지하고 싶을 때 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// NoInfer가 없는 경우
function createFSM<S extends string>(config: {
  initial: S;
  states: Record<S, { onEnter?: () => void }>;
}) {}

// 문제: 'idle' | 'running' | 'typo'로 추론됨
createFSM({
  initial: 'idle',
  states: {
    idle: {},
    running: {},
    typo: {} // 오타지만 타입 추론에 포함됨
  }
});

// NoInfer 사용
function createFSMSafe<S extends string>(config: {
  initial: NoInfer<S>;  // 여기서는 추론하지 않음
  states: Record<S, { onEnter?: () => void }>;
}) {}

// 이제 states의 키만으로 S가 추론되고, initial은 그 타입을 따라야 함
createFSMSafe({
  initial: 'idle',
  states: {
    idle: {},
    running: {}
  }
});

// 오류: 'typo'는 'idle' | 'running'에 없음
createFSMSafe({
  initial: 'typo', // Error!
  states: {
    idle: {},
    running: {}
  }
});

실전 활용: 기본값 설정 함수

1
2
3
4
5
6
7
8
9
10
11
12
function withDefault<T>(
  value: T | undefined,
  defaultValue: NoInfer<T>  // defaultValue에서 T를 추론하지 않음
): T {
  return value ?? defaultValue;
}

// T는 value의 타입(string)으로 추론됨
const result = withDefault<string>(undefined, 'default');

// 오류: defaultValue가 value의 타입과 일치하지 않음
const wrong = withDefault('hello', 42); // Error: 42는 string이 아님

5.4: 클로저에서 narrowed types 보존

이전 버전에서는 콜백 함수 내에서 타입 narrowing이 유지되지 않는 경우가 있었습니다. 5.4에서 이 문제가 해결되었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function processData(data: string | number | null) {
  if (data === null) {
    return;
  }

  // 이전: 콜백 내에서 data가 string | number | null로 돌아감
  // 5.4+: data가 string | number로 유지됨
  setTimeout(() => {
    console.log(data.toString()); // 이제 오류 없음!
  }, 1000);

  // 배열 메서드에서도 마찬가지
  [1, 2, 3].forEach(() => {
    if (typeof data === 'string') {
      console.log(data.toUpperCase());
    }
  });
}

5.4: Object.groupBy / Map.groupBy 타입 지원

ES2024의 새로운 그룹화 메서드에 대한 타입 지원이 추가되었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
interface Product {
  id: number;
  name: string;
  category: string;
  price: number;
}

const products: Product[] = [
  { id: 1, name: 'Laptop', category: 'electronics', price: 999 },
  { id: 2, name: 'Phone', category: 'electronics', price: 699 },
  { id: 3, name: 'Shirt', category: 'clothing', price: 29 },
  { id: 4, name: 'Pants', category: 'clothing', price: 49 }
];

// Object.groupBy - 객체로 그룹화
const groupedByCategory = Object.groupBy(products, (product) => product.category);
// 타입: Partial<Record<string, Product[]>>

console.log(groupedByCategory.electronics);
// [{ id: 1, name: 'Laptop', ... }, { id: 2, name: 'Phone', ... }]

// Map.groupBy - Map으로 그룹화
const mapByCategory = Map.groupBy(products, (product) => product.category);
// 타입: Map<string, Product[]>

for (const [category, items] of mapByCategory) {
  console.log(`${category}: ${items.length} items`);
}

// 복잡한 키로 그룹화
const groupedByPriceRange = Object.groupBy(products, (product) => {
  if (product.price < 50) return 'budget';
  if (product.price < 500) return 'mid';
  return 'premium';
});

5.5: Inferred Type Predicates

TypeScript 5.5는 타입 가드 함수의 반환 타입을 자동으로 추론합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 5.5 이전: 명시적 타입 가드 필요
function isStringOld(value: unknown): value is string {
  return typeof value === 'string';
}

// 5.5+: 타입 가드가 자동으로 추론됨!
function isString(value: unknown) {
  return typeof value === 'string';
}
// 추론된 타입: (value: unknown) => value is string

// 배열 필터링에서 자동으로 타입이 좁혀짐
const mixed: (string | number | null)[] = ['a', 1, null, 'b', 2];

const strings = mixed.filter((item) => typeof item === 'string');
// 5.5+: strings의 타입은 string[]
// 이전: (string | number | null)[]

복잡한 타입 가드 자동 추론

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface User {
  id: number;
  name: string;
  email: string;
}

interface Admin extends User {
  role: 'admin';
  permissions: string[];
}

// 자동으로 타입 가드로 인식됨
function isAdmin(user: User | Admin) {
  return 'role' in user && user.role === 'admin';
}
// 추론: (user: User | Admin) => user is Admin

const users: (User | Admin)[] = [
  { id: 1, name: 'John', email: 'john@example.com' },
  { id: 2, name: 'Jane', email: 'jane@example.com', role: 'admin', permissions: ['read', 'write'] }
];

const admins = users.filter(isAdmin);
// admins의 타입: Admin[]

5.5: 정규식 문법 체크

TypeScript 5.5는 정규식 리터럴의 문법을 컴파일 타임에 검사합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 컴파일 오류: 잘못된 정규식
const badRegex = /[a-Z]/;  // Error: Invalid character class range

// 올바른 정규식
const goodRegex = /[a-zA-Z]/;

// 잘못된 그룹 참조
const badGroup = /(\w+) \2/;  // Error: \2 참조하지만 그룹이 1개뿐

// 올바른 그룹 참조
const goodGroup = /(\w+) \1/;

// 잘못된 플래그 조합
const badFlags = /test/uu;  // Error: 중복 플래그

5.5: 새로운 Set 메서드 타입

ES2025의 새로운 Set 메서드들에 대한 타입이 추가되었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);

// 합집합
const union = setA.union(setB);
// Set { 1, 2, 3, 4, 5, 6 }

// 교집합
const intersection = setA.intersection(setB);
// Set { 3, 4 }

// 차집합
const difference = setA.difference(setB);
// Set { 1, 2 }

// 대칭 차집합
const symmetricDifference = setA.symmetricDifference(setB);
// Set { 1, 2, 5, 6 }

// 부분집합/상위집합 체크
const isSubset = setA.isSubsetOf(new Set([1, 2, 3, 4, 5]));
const isSuperset = setA.isSupersetOf(new Set([1, 2]));
const isDisjoint = setA.isDisjointFrom(new Set([7, 8, 9]));

5.6: Disallowed Nullish and Truthy Checks

TypeScript 5.6은 항상 참이거나 거짓인 조건문을 오류로 처리합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function example(value: string) {
  // 오류: 이 조건은 항상 참
  if (value !== null) {  // string 타입은 null이 될 수 없음
    console.log(value);
  }

  // 오류: 이 조건은 항상 거짓
  if (value === undefined) {  // string 타입은 undefined가 될 수 없음
    console.log('never');
  }
}

// 실수 방지 예시
function processUser(user: { name: string }) {
  // 5.6 이전: 경고 없음
  // 5.6+: 오류 - user.name은 항상 truthy가 아닐 수 있음 (빈 문자열)
  if (user) {  // 이 조건은 항상 참 (user는 객체)
    console.log(user.name);
  }
}

5.6: Iterator Helper Methods

ES2025 Iterator Helpers에 대한 타입 지원이 추가되었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 이터레이터 생성
function* numbers() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
}

// Iterator Helper Methods 사용
const doubled = numbers()
  .map(n => n * 2)
  .filter(n => n > 4)
  .take(2)
  .toArray();
// [6, 8]

// 무한 이터레이터도 안전하게 처리
function* infiniteNumbers() {
  let n = 0;
  while (true) {
    yield n++;
  }
}

const firstTen = infiniteNumbers()
  .take(10)
  .toArray();
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

// drop과 take 조합
const middleNumbers = infiniteNumbers()
  .drop(5)
  .take(5)
  .toArray();
// [5, 6, 7, 8, 9]

실무에서 자주 쓰는 유틸리티 타입

기본 유틸리티 타입 심화

Partial: 모든 속성을 선택적으로

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// 모든 속성이 선택적
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; age?: number; }

// 실제 사용: 부분 업데이트
function updateUser(id: number, updates: Partial<User>): User {
  const user = getUser(id);
  return { ...user, ...updates };
}

updateUser(1, { name: 'New Name' });  // name만 업데이트
updateUser(1, { email: 'new@email.com', age: 30 });  // email, age 업데이트

Required: 모든 속성을 필수로

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Config {
  host?: string;
  port?: number;
  ssl?: boolean;
}

// 모든 속성이 필수
type RequiredConfig = Required<Config>;
// { host: string; port: number; ssl: boolean; }

// 실제 사용: 기본값 적용 후 타입
function createServer(config: Config): RequiredConfig {
  return {
    host: config.host ?? 'localhost',
    port: config.port ?? 3000,
    ssl: config.ssl ?? false
  };
}

Pick<T, K>: 특정 속성만 선택

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Article {
  id: number;
  title: string;
  content: string;
  author: string;
  createdAt: Date;
  updatedAt: Date;
}

// 목록에 필요한 속성만 선택
type ArticleListItem = Pick<Article, 'id' | 'title' | 'author' | 'createdAt'>;

// API 응답 타입 정의
async function getArticleList(): Promise<ArticleListItem[]> {
  const response = await fetch('/api/articles');
  return response.json();
}

Omit<T, K>: 특정 속성 제외

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface FullProduct {
  id: number;
  name: string;
  price: number;
  description: string;
  internalCode: string;  // 내부용
  costPrice: number;     // 내부용
}

// 외부에 노출할 타입 (내부 정보 제외)
type PublicProduct = Omit<FullProduct, 'internalCode' | 'costPrice'>;

// 생성용 타입 (id 제외)
type CreateProductDTO = Omit<FullProduct, 'id'>;

Record<K, T>: 키-값 맵 타입

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 문자열 키, 특정 값 타입
type StatusMessages = Record<'success' | 'error' | 'loading', string>;

const messages: StatusMessages = {
  success: '성공적으로 처리되었습니다.',
  error: '오류가 발생했습니다.',
  loading: '처리 중입니다...'
};

// 객체를 딕셔너리로 사용
type UserMap = Record<string, User>;

const users: UserMap = {
  'user-1': { id: 1, name: 'John', email: 'john@test.com', age: 30 },
  'user-2': { id: 2, name: 'Jane', email: 'jane@test.com', age: 25 }
};

// 열거형 키와 함께 사용
enum Permission {
  Read = 'READ',
  Write = 'WRITE',
  Delete = 'DELETE'
}

type PermissionDescriptions = Record<Permission, string>;

const descriptions: PermissionDescriptions = {
  [Permission.Read]: '데이터를 읽을 수 있습니다.',
  [Permission.Write]: '데이터를 수정할 수 있습니다.',
  [Permission.Delete]: '데이터를 삭제할 수 있습니다.'
};

고급 유틸리티 타입

ReturnType: 함수 반환 타입 추출

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function fetchUser(id: number) {
  return {
    id,
    name: 'John',
    email: 'john@test.com',
    createdAt: new Date()
  };
}

// 함수의 반환 타입 추출
type User = ReturnType<typeof fetchUser>;
// { id: number; name: string; email: string; createdAt: Date; }

// 비동기 함수의 경우
async function fetchUserAsync(id: number) {
  const response = await fetch(`/api/users/${id}`);
  return response.json() as Promise<{ id: number; name: string }>;
}

type AsyncUser = Awaited<ReturnType<typeof fetchUserAsync>>;
// { id: number; name: string; }

Parameters: 함수 매개변수 타입 추출

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function createUser(name: string, email: string, age?: number) {
  return { name, email, age };
}

// 함수의 매개변수 타입을 튜플로 추출
type CreateUserParams = Parameters<typeof createUser>;
// [name: string, email: string, age?: number]

// 특정 매개변수만 추출
type FirstParam = Parameters<typeof createUser>[0];  // string
type SecondParam = Parameters<typeof createUser>[1]; // string

// 래퍼 함수 만들기
function loggedCreateUser(...args: Parameters<typeof createUser>) {
  console.log('Creating user with:', args);
  return createUser(...args);
}

Awaited: Promise 해제 타입

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type PromiseString = Promise<string>;
type ResolvedString = Awaited<PromiseString>;  // string

// 중첩된 Promise도 처리
type NestedPromise = Promise<Promise<Promise<number>>>;
type ResolvedNumber = Awaited<NestedPromise>;  // number

// 실제 활용: API 응답 타입
async function fetchData(): Promise<{ data: string[] }> {
  return { data: ['a', 'b', 'c'] };
}

type FetchResult = Awaited<ReturnType<typeof fetchData>>;
// { data: string[] }

NoInfer: 타입 추론 제어 (5.4+)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 이벤트 시스템 예제
function on<T extends string>(
  event: T,
  callback: (data: NoInfer<T>) => void
) {}

// T는 첫 번째 인자에서만 추론됨
on('click', (data) => {
  // data의 타입은 'click'
});

// 상태 머신 예제
function createStateMachine<State extends string>(config: {
  initial: NoInfer<State>;
  states: Record<State, { on?: Record<string, State> }>;
}) {}

createStateMachine({
  initial: 'idle',  // 'states'의 키에서 추론된 타입이어야 함
  states: {
    idle: { on: { START: 'running' } },
    running: { on: { STOP: 'idle' } }
  }
});

조건부 타입

Exclude<T, U>: T에서 U 제외

1
2
3
4
5
6
7
8
type AllTypes = string | number | boolean | null | undefined;
type PrimitiveTypes = Exclude<AllTypes, null | undefined>;
// string | number | boolean

// 실제 활용: 특정 상태 제외
type Status = 'pending' | 'approved' | 'rejected' | 'cancelled';
type ActiveStatus = Exclude<Status, 'cancelled'>;
// 'pending' | 'approved' | 'rejected'

Extract<T, U>: T에서 U만 추출

1
2
3
4
5
6
7
8
type AllTypes = string | number | boolean | (() => void);
type Functions = Extract<AllTypes, Function>;
// () => void

// 실제 활용: 특정 패턴의 키만 추출
type Keys = 'onClick' | 'onSubmit' | 'className' | 'style';
type EventKeys = Extract<Keys, `on${string}`>;
// 'onClick' | 'onSubmit'

NonNullable: null과 undefined 제거

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// string

// 실제 활용: API 응답 정제
interface ApiResponse {
  data?: {
    user?: {
      name: string;
      email?: string | null;
    } | null;
  } | null;
}

function processUser(response: ApiResponse) {
  const user = response.data?.user;
  if (user) {
    // user의 타입: { name: string; email?: string | null }
    const name: string = user.name;
    const email: NonNullable<typeof user.email> | undefined =
      user.email ?? undefined;
  }
}

문자열 조작 타입

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 대문자 변환
type Uppercase<T extends string> = ...
type Upper = Uppercase<'hello'>;  // 'HELLO'

// 소문자 변환
type Lowercase<T extends string> = ...
type Lower = Lowercase<'HELLO'>;  // 'hello'

// 첫 글자만 대문자
type Capitalize<T extends string> = ...
type Cap = Capitalize<'hello'>;  // 'Hello'

// 첫 글자만 소문자
type Uncapitalize<T extends string> = ...
type Uncap = Uncapitalize<'HELLO'>;  // 'hELLO'

// 실제 활용: 이벤트 핸들러 타입 생성
type EventName = 'click' | 'change' | 'submit';
type EventHandler<T extends string> = `on${Capitalize<T>}`;
type Handlers = EventHandler<EventName>;
// 'onClick' | 'onChange' | 'onSubmit'

// 실제 활용: API 엔드포인트 타입
type Resource = 'user' | 'post' | 'comment';
type Endpoint = `/${Lowercase<Resource>}s`;
// '/users' | '/posts' | '/comments'

커스텀 유틸리티 타입 만들기 (실전 예제)

1. DeepPartial: 중첩 객체도 모두 선택적으로

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
type DeepPartial<T> = T extends object
  ? { [P in keyof T]?: DeepPartial<T[P]> }
  : T;

// 사용 예시
interface DeepConfig {
  server: {
    host: string;
    port: number;
    ssl: {
      enabled: boolean;
      cert: string;
    };
  };
  database: {
    url: string;
    poolSize: number;
  };
}

type PartialConfig = DeepPartial<DeepConfig>;
// 모든 중첩 속성이 선택적

const partialConfig: PartialConfig = {
  server: {
    ssl: {
      enabled: true
      // cert는 선택적
    }
    // host, port는 선택적
  }
  // database는 선택적
};

2. DeepReadonly: 중첩 객체도 모두 읽기 전용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
type DeepReadonly<T> = T extends (infer U)[]
  ? ReadonlyArray<DeepReadonly<U>>
  : T extends object
  ? { readonly [P in keyof T]: DeepReadonly<T[P]> }
  : T;

// 사용 예시
interface AppState {
  user: {
    name: string;
    preferences: {
      theme: 'light' | 'dark';
      notifications: boolean;
    };
  };
  posts: { id: number; title: string }[];
}

type ImmutableState = DeepReadonly<AppState>;

const state: ImmutableState = {
  user: {
    name: 'John',
    preferences: { theme: 'dark', notifications: true }
  },
  posts: [{ id: 1, title: 'Hello' }]
};

// state.user.name = 'Jane';  // 오류!
// state.posts.push({ id: 2, title: 'World' });  // 오류!

3. RequiredKeys / OptionalKeys: 필수/선택 키 추출

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type RequiredKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];

type OptionalKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];

// 사용 예시
interface User {
  id: number;
  name: string;
  email?: string;
  age?: number;
}

type Required = RequiredKeys<User>;  // 'id' | 'name'
type Optional = OptionalKeys<User>;  // 'email' | 'age'

4. Mutable: Readonly 제거

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

// 중첩된 경우
type DeepMutable<T> = T extends object
  ? { -readonly [P in keyof T]: DeepMutable<T[P]> }
  : T;

// 사용 예시
interface ReadonlyUser {
  readonly id: number;
  readonly name: string;
}

type MutableUser = Mutable<ReadonlyUser>;
// { id: number; name: string; }

5. PickByType: 특정 타입의 속성만 선택

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type PickByType<T, V> = {
  [K in keyof T as T[K] extends V ? K : never]: T[K];
};

// 사용 예시
interface Mixed {
  id: number;
  name: string;
  active: boolean;
  count: number;
  description: string;
}

type StringProps = PickByType<Mixed, string>;
// { name: string; description: string; }

type NumberProps = PickByType<Mixed, number>;
// { id: number; count: number; }

6. OmitByType: 특정 타입의 속성 제외

1
2
3
4
5
6
7
type OmitByType<T, V> = {
  [K in keyof T as T[K] extends V ? never : K]: T[K];
};

// 사용 예시
type WithoutStrings = OmitByType<Mixed, string>;
// { id: number; active: boolean; count: number; }

7. UnionToIntersection: 유니온을 교차 타입으로

1
2
3
4
5
6
7
8
9
10
type UnionToIntersection<U> = (
  U extends any ? (x: U) => void : never
) extends (x: infer I) => void
  ? I
  : never;

// 사용 예시
type Union = { a: string } | { b: number } | { c: boolean };
type Intersection = UnionToIntersection<Union>;
// { a: string } & { b: number } & { c: boolean }

제네릭 고급 활용법

제네릭 기본 복습과 심화

제네릭은 타입을 매개변수화하여 재사용 가능한 컴포넌트를 만드는 핵심 기능입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 기본 제네릭 함수
function identity<T>(value: T): T {
  return value;
}

// 타입 추론
const str = identity('hello');  // string으로 추론
const num = identity(42);       // number로 추론

// 명시적 타입 지정
const explicit = identity<string>('hello');

// 제네릭 인터페이스
interface Container<T> {
  value: T;
  getValue(): T;
  setValue(value: T): void;
}

// 제네릭 클래스
class Box<T> {
  constructor(private value: T) {}

  get(): T {
    return this.value;
  }

  set(value: T): void {
    this.value = value;
  }
}

제네릭 제약조건 (extends)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 특정 속성을 가진 타입으로 제한
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(value: T): T {
  console.log(value.length);
  return value;
}

logLength('hello');      // OK: string has length
logLength([1, 2, 3]);    // OK: array has length
logLength({ length: 5 }); // OK: object has length
// logLength(42);        // Error: number doesn't have length

// 객체 키 제약
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: 'John', age: 30 };
const name = getProperty(user, 'name');  // string
const age = getProperty(user, 'age');    // number
// getProperty(user, 'email');  // Error: 'email' is not a key of user

다중 제네릭과 기본값

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 다중 제네릭
function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

const merged = merge({ name: 'John' }, { age: 30 });
// { name: string; age: number }

// 제네릭 기본값
interface ApiResponse<T = unknown, E = Error> {
  data: T | null;
  error: E | null;
  status: number;
}

// 기본값 사용
const response1: ApiResponse = { data: null, error: null, status: 200 };

// 명시적 지정
const response2: ApiResponse<User> = {
  data: { id: 1, name: 'John', email: 'john@test.com', age: 30 },
  error: null,
  status: 200
};

// 에러 타입도 지정
interface CustomError {
  code: string;
  message: string;
}

const response3: ApiResponse<User, CustomError> = {
  data: null,
  error: { code: 'NOT_FOUND', message: 'User not found' },
  status: 404
};

조건부 타입과 제네릭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 기본 조건부 타입
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false

// 조건부 타입으로 타입 변환
type Flatten<T> = T extends Array<infer U> ? U : T;

type Str = Flatten<string[]>;     // string
type Num = Flatten<number>;       // number
type Mixed = Flatten<(string | number)[]>;  // string | number

// 실제 활용: 응답 타입 추출
type ApiEndpoints = {
  '/users': User[];
  '/users/:id': User;
  '/posts': Post[];
  '/posts/:id': Post;
};

type GetResponse<T extends keyof ApiEndpoints> = ApiEndpoints[T];

type UsersResponse = GetResponse<'/users'>;  // User[]
type UserResponse = GetResponse<'/users/:id'>;  // User

infer 키워드 마스터하기

infer는 조건부 타입 내에서 타입을 추론하고 캡처하는 강력한 도구입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 함수 반환 타입 추출 (ReturnType 구현)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// 함수 매개변수 타입 추출 (Parameters 구현)
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;

// 첫 번째 매개변수만 추출
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any
  ? F
  : never;

function greet(name: string, age: number) {
  return `Hello, ${name}`;
}

type FirstArgType = FirstArg<typeof greet>;  // string

// Promise 내부 타입 추출 (Awaited 구현)
type UnwrapPromise<T> = T extends Promise<infer U>
  ? UnwrapPromise<U>  // 재귀적으로 중첩 Promise 해제
  : T;

type Resolved = UnwrapPromise<Promise<Promise<string>>>;  // string

// 배열 요소 타입 추출
type ArrayElement<T> = T extends (infer E)[] ? E : T;

type Element = ArrayElement<string[]>;  // string

// 객체의 특정 메서드 반환 타입 추출
type MethodReturnType<T, M extends keyof T> = T[M] extends (...args: any[]) => infer R
  ? R
  : never;

interface UserService {
  getUser(id: number): Promise<User>;
  createUser(data: Partial<User>): Promise<User>;
  deleteUser(id: number): Promise<boolean>;
}

type GetUserReturn = MethodReturnType<UserService, 'getUser'>;
// Promise<User>

Mapped Types와 제네릭 결합

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 모든 속성을 특정 타입으로 변환
type AllStrings<T> = {
  [K in keyof T]: string;
};

interface User {
  id: number;
  name: string;
  active: boolean;
}

type UserStrings = AllStrings<User>;
// { id: string; name: string; active: string; }

// 속성 이름 변환
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; getActive: () => boolean; }

// Setters 생성
type Setters<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

type UserSetters = Setters<User>;
// { setId: (value: number) => void; setName: (value: string) => void; ... }

// 특정 속성만 선택적으로 만들기
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

type UserWithOptionalEmail = PartialBy<User, 'id'>;
// { name: string; active: boolean; id?: number; }

// 특정 속성만 필수로 만들기
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;

재귀적 타입 정의

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// JSON 타입 정의
type JsonPrimitive = string | number | boolean | null;
type JsonArray = JsonValue[];
type JsonObject = { [key: string]: JsonValue };
type JsonValue = JsonPrimitive | JsonArray | JsonObject;

// 사용 예시
const jsonData: JsonValue = {
  name: 'John',
  age: 30,
  tags: ['developer', 'typescript'],
  address: {
    city: 'Seoul',
    zip: null
  }
};

// 중첩 경로 타입
type Path<T, K extends keyof T = keyof T> = K extends string
  ? T[K] extends Record<string, any>
    ? K | `${K}.${Path<T[K]>}`
    : K
  : never;

interface NestedObject {
  a: {
    b: {
      c: string;
    };
    d: number;
  };
  e: boolean;
}

type Paths = Path<NestedObject>;
// 'a' | 'e' | 'a.b' | 'a.d' | 'a.b.c'

실전 패턴: API 응답 타입 추론

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// API 엔드포인트 정의
interface ApiDefinition {
  '/users': {
    GET: { response: User[] };
    POST: { body: CreateUserDTO; response: User };
  };
  '/users/:id': {
    GET: { response: User };
    PUT: { body: UpdateUserDTO; response: User };
    DELETE: { response: void };
  };
}

// 응답 타입 추출
type ApiResponse<
  Path extends keyof ApiDefinition,
  Method extends keyof ApiDefinition[Path]
> = ApiDefinition[Path][Method] extends { response: infer R } ? R : never;

// 요청 바디 타입 추출
type ApiBody<
  Path extends keyof ApiDefinition,
  Method extends keyof ApiDefinition[Path]
> = ApiDefinition[Path][Method] extends { body: infer B } ? B : never;

// 사용 예시
type UsersGetResponse = ApiResponse<'/users', 'GET'>;  // User[]
type UserPostBody = ApiBody<'/users', 'POST'>;  // CreateUserDTO

// 타입 안전한 fetch 래퍼
async function api<
  P extends keyof ApiDefinition,
  M extends keyof ApiDefinition[P]
>(
  path: P,
  method: M,
  body?: ApiBody<P, M>
): Promise<ApiResponse<P, M>> {
  const response = await fetch(path as string, {
    method: method as string,
    body: body ? JSON.stringify(body) : undefined
  });
  return response.json();
}

실전 패턴: 폼 유효성 검사 타입

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 검증 규칙 정의
type ValidationRule<T> = {
  validate: (value: T) => boolean;
  message: string;
};

// 필드별 검증 스키마
type ValidationSchema<T> = {
  [K in keyof T]?: ValidationRule<T[K]>[];
};

// 검증 결과
type ValidationResult<T> = {
  [K in keyof T]?: string[];
};

// 폼 데이터 타입
interface LoginForm {
  email: string;
  password: string;
  rememberMe: boolean;
}

// 검증 스키마 정의
const loginSchema: ValidationSchema<LoginForm> = {
  email: [
    {
      validate: (v) => v.length > 0,
      message: '이메일을 입력해주세요.'
    },
    {
      validate: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
      message: '올바른 이메일 형식이 아닙니다.'
    }
  ],
  password: [
    {
      validate: (v) => v.length >= 8,
      message: '비밀번호는 8자 이상이어야 합니다.'
    }
  ]
};

// 검증 함수
function validate<T extends Record<string, any>>(
  data: T,
  schema: ValidationSchema<T>
): ValidationResult<T> {
  const result: ValidationResult<T> = {};

  for (const key in schema) {
    const rules = schema[key];
    if (!rules) continue;

    const errors: string[] = [];
    for (const rule of rules) {
      if (!rule.validate(data[key])) {
        errors.push(rule.message);
      }
    }

    if (errors.length > 0) {
      result[key] = errors;
    }
  }

  return result;
}

타입 안정성을 높이는 패턴들

Branded Types (Nominal Typing)

TypeScript는 구조적 타입 시스템을 사용하므로, 같은 구조의 타입은 호환됩니다. Branded Types를 사용하면 구조는 같지만 의미가 다른 타입을 구분할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 브랜드 타입 정의
declare const brand: unique symbol;

type Brand<T, B> = T & { [brand]: B };

// 특정 도메인 타입 정의
type UserId = Brand<string, 'UserId'>;
type PostId = Brand<string, 'PostId'>;
type Email = Brand<string, 'Email'>;

// 생성 함수 (유효성 검사 포함)
function createUserId(id: string): UserId {
  if (!id.startsWith('user_')) {
    throw new Error('Invalid user ID format');
  }
  return id as UserId;
}

function createEmail(email: string): Email {
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    throw new Error('Invalid email format');
  }
  return email as Email;
}

// 사용 예시
function getUser(id: UserId): User {
  // ...
  return {} as User;
}

function getPost(id: PostId): Post {
  // ...
  return {} as Post;
}

const userId = createUserId('user_123');
const postId = 'post_456' as PostId;

getUser(userId);  // OK
// getUser(postId);  // Error: PostId는 UserId에 할당할 수 없음
// getUser('user_123');  // Error: string은 UserId에 할당할 수 없음

실전 예제: 금액 타입

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type KRW = Brand<number, 'KRW'>;
type USD = Brand<number, 'USD'>;

function krw(amount: number): KRW {
  return amount as KRW;
}

function usd(amount: number): USD {
  return amount as USD;
}

function addKRW(a: KRW, b: KRW): KRW {
  return (a + b) as KRW;
}

const price1 = krw(10000);
const price2 = krw(5000);
const total = addKRW(price1, price2);  // OK

const dollars = usd(100);
// addKRW(price1, dollars);  // Error: USD는 KRW에 할당할 수 없음

Exhaustive Check

switch문에서 모든 케이스를 처리했는지 컴파일 타임에 확인합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
type Status = 'pending' | 'approved' | 'rejected';

function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${value}`);
}

function getStatusMessage(status: Status): string {
  switch (status) {
    case 'pending':
      return '검토 중입니다.';
    case 'approved':
      return '승인되었습니다.';
    case 'rejected':
      return '거절되었습니다.';
    default:
      // 모든 케이스를 처리하면 여기에 도달하지 않음
      return assertNever(status);
  }
}

// 새로운 상태가 추가되면 컴파일 오류 발생
type NewStatus = 'pending' | 'approved' | 'rejected' | 'cancelled';

function getNewStatusMessage(status: NewStatus): string {
  switch (status) {
    case 'pending':
      return '검토 중입니다.';
    case 'approved':
      return '승인되었습니다.';
    case 'rejected':
      return '거절되었습니다.';
    // 'cancelled' 케이스가 없으면 컴파일 오류!
    // case 'cancelled':
    //   return '취소되었습니다.';
    default:
      return assertNever(status);
      // Error: 'cancelled' is not assignable to 'never'
  }
}

Type Guards

기본 타입 가드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// typeof 가드
function processValue(value: string | number) {
  if (typeof value === 'string') {
    // value는 string
    console.log(value.toUpperCase());
  } else {
    // value는 number
    console.log(value.toFixed(2));
  }
}

// instanceof 가드
class Dog {
  bark() { console.log('Woof!'); }
}

class Cat {
  meow() { console.log('Meow!'); }
}

function makeSound(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark();
  } else {
    animal.meow();
  }
}

// in 가드
interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

function move(animal: Bird | Fish) {
  if ('fly' in animal) {
    animal.fly();
  } else {
    animal.swim();
  }
}

사용자 정의 타입 가드 (is 키워드)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
interface User {
  type: 'user';
  name: string;
  email: string;
}

interface Admin {
  type: 'admin';
  name: string;
  email: string;
  permissions: string[];
}

// 타입 가드 함수
function isAdmin(person: User | Admin): person is Admin {
  return person.type === 'admin';
}

function processAccess(person: User | Admin) {
  if (isAdmin(person)) {
    // person은 Admin
    console.log(`Admin permissions: ${person.permissions.join(', ')}`);
  } else {
    // person은 User
    console.log(`Regular user: ${person.name}`);
  }
}

// 배열 필터링에서 타입 가드 활용
const people: (User | Admin)[] = [
  { type: 'user', name: 'John', email: 'john@test.com' },
  { type: 'admin', name: 'Jane', email: 'jane@test.com', permissions: ['read', 'write'] }
];

const admins = people.filter(isAdmin);  // Admin[]

assertion 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error(`Expected string, got ${typeof value}`);
  }
}

function processInput(input: unknown) {
  assertIsString(input);
  // 이후로 input은 string
  console.log(input.toUpperCase());
}

// null/undefined 검사
function assertDefined<T>(value: T): asserts value is NonNullable<T> {
  if (value === null || value === undefined) {
    throw new Error('Value is null or undefined');
  }
}

function process(data: string | null | undefined) {
  assertDefined(data);
  // data는 string
  console.log(data.length);
}

Discriminated Unions (태그된 유니온)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 공통 판별자 속성을 가진 타입들
interface LoadingState {
  status: 'loading';
}

interface SuccessState<T> {
  status: 'success';
  data: T;
}

interface ErrorState {
  status: 'error';
  error: string;
}

type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;

// 타입 안전한 상태 처리
function renderData<T>(state: AsyncState<T>): string {
  switch (state.status) {
    case 'loading':
      return 'Loading...';
    case 'success':
      return `Data: ${JSON.stringify(state.data)}`;
    case 'error':
      return `Error: ${state.error}`;
  }
}

// 실전 예제: Redux 액션 타입
interface FetchUserRequest {
  type: 'FETCH_USER_REQUEST';
}

interface FetchUserSuccess {
  type: 'FETCH_USER_SUCCESS';
  payload: User;
}

interface FetchUserFailure {
  type: 'FETCH_USER_FAILURE';
  error: string;
}

type UserAction = FetchUserRequest | FetchUserSuccess | FetchUserFailure;

function userReducer(state: User | null, action: UserAction): User | null {
  switch (action.type) {
    case 'FETCH_USER_REQUEST':
      return null;
    case 'FETCH_USER_SUCCESS':
      return action.payload;
    case 'FETCH_USER_FAILURE':
      console.error(action.error);
      return null;
  }
}

Const Assertions (as const)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// as const 없이
const config1 = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
};
// 타입: { apiUrl: string; timeout: number; retries: number; }

// as const 사용
const config2 = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
} as const;
// 타입: { readonly apiUrl: 'https://api.example.com'; readonly timeout: 5000; readonly retries: 3; }

// 배열에서의 차이
const colors1 = ['red', 'green', 'blue'];
// 타입: string[]

const colors2 = ['red', 'green', 'blue'] as const;
// 타입: readonly ['red', 'green', 'blue']

// 튜플 타입 생성
const point = [10, 20] as const;  // readonly [10, 20]

// 열거형 대안으로 사용
const Status = {
  Pending: 'PENDING',
  Approved: 'APPROVED',
  Rejected: 'REJECTED'
} as const;

type StatusType = typeof Status[keyof typeof Status];
// 'PENDING' | 'APPROVED' | 'REJECTED'

Template Literal Types

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 기본 템플릿 리터럴 타입
type Greeting = `Hello, ${string}!`;

const greet1: Greeting = 'Hello, World!';  // OK
const greet2: Greeting = 'Hello, TypeScript!';  // OK
// const greet3: Greeting = 'Hi, World!';  // Error

// 조합 타입
type Color = 'red' | 'green' | 'blue';
type Size = 'small' | 'medium' | 'large';

type ClassName = `${Size}-${Color}`;
// 'small-red' | 'small-green' | 'small-blue' | 'medium-red' | ...

// 이벤트 핸들러 타입
type ElementEvents = 'click' | 'focus' | 'blur' | 'change';
type EventHandler = `on${Capitalize<ElementEvents>}`;
// 'onClick' | 'onFocus' | 'onBlur' | 'onChange'

// CSS 속성 타입
type CSSUnit = 'px' | 'em' | 'rem' | '%';
type CSSValue = `${number}${CSSUnit}`;

const width: CSSValue = '100px';  // OK
const height: CSSValue = '50%';   // OK
// const invalid: CSSValue = '100';  // Error

// API 경로 타입
type ApiVersion = 'v1' | 'v2';
type Resource = 'users' | 'posts' | 'comments';
type ApiPath = `/api/${ApiVersion}/${Resource}`;
// '/api/v1/users' | '/api/v1/posts' | '/api/v1/comments' | ...

// 경로 매개변수 추출
type ExtractParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<Rest>
    : T extends `${string}:${infer Param}`
    ? Param
    : never;

type Params = ExtractParams<'/users/:userId/posts/:postId'>;
// 'userId' | 'postId'

Strict Null Checks 패턴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 옵셔널 체이닝과 널 병합
interface UserProfile {
  name: string;
  address?: {
    city?: string;
    country?: string;
  };
  contacts?: {
    email?: string;
    phone?: string;
  }[];
}

function getCity(user: UserProfile): string {
  // 옵셔널 체이닝 + 널 병합
  return user.address?.city ?? 'Unknown';
}

function getFirstEmail(user: UserProfile): string | undefined {
  // 배열의 첫 요소에 안전하게 접근
  return user.contacts?.[0]?.email;
}

// Non-null assertion (!)은 가능한 피하기
function processUser(user: UserProfile | null) {
  // ❌ 위험: user가 null이면 런타임 오류
  // console.log(user!.name);

  // ✅ 안전: 먼저 검사
  if (user) {
    console.log(user.name);
  }
}

// 널 체크 유틸리티 함수
function isNotNullish<T>(value: T): value is NonNullable<T> {
  return value !== null && value !== undefined;
}

const values = [1, null, 2, undefined, 3];
const numbers = values.filter(isNotNullish);  // number[]

타입 단언 최소화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// ❌ 나쁜 예: 과도한 타입 단언
function processData(data: unknown) {
  const user = data as User;
  console.log(user.name);
}

// ✅ 좋은 예: 타입 가드 사용
function isUser(data: unknown): data is User {
  return (
    typeof data === 'object' &&
    data !== null &&
    'name' in data &&
    'email' in data &&
    typeof (data as User).name === 'string' &&
    typeof (data as User).email === 'string'
  );
}

function processDataSafe(data: unknown) {
  if (isUser(data)) {
    console.log(data.name);  // 안전!
  } else {
    console.log('Invalid user data');
  }
}

// ✅ Zod 같은 런타임 검증 라이브러리 활용
import { z } from 'zod';

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  age: z.number().optional()
});

type UserFromSchema = z.infer<typeof UserSchema>;

function processWithZod(data: unknown) {
  const result = UserSchema.safeParse(data);
  if (result.success) {
    const user = result.data;  // UserFromSchema 타입
    console.log(user.name);
  } else {
    console.log('Validation errors:', result.error);
  }
}

실전 프로젝트 패턴

API 타입 안전하게 관리하기 (fetch wrapper)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// API 응답 타입 정의
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

interface ApiError {
  code: string;
  message: string;
  details?: Record<string, string[]>;
}

// API 엔드포인트 정의
interface ApiEndpoints {
  'GET /users': {
    response: User[];
    query?: { page?: number; limit?: number };
  };
  'GET /users/:id': {
    response: User;
    params: { id: string };
  };
  'POST /users': {
    response: User;
    body: CreateUserDTO;
  };
  'PUT /users/:id': {
    response: User;
    params: { id: string };
    body: UpdateUserDTO;
  };
  'DELETE /users/:id': {
    response: void;
    params: { id: string };
  };
}

// 엔드포인트에서 HTTP 메서드와 경로 추출
type ExtractMethod<T extends string> = T extends `${infer M} ${string}` ? M : never;
type ExtractPath<T extends string> = T extends `${string} ${infer P}` ? P : never;

// 타입 안전한 fetch 클라이언트
class ApiClient {
  constructor(private baseUrl: string) {}

  async request<K extends keyof ApiEndpoints>(
    endpoint: K,
    options?: {
      params?: ApiEndpoints[K] extends { params: infer P } ? P : never;
      query?: ApiEndpoints[K] extends { query: infer Q } ? Q : never;
      body?: ApiEndpoints[K] extends { body: infer B } ? B : never;
    }
  ): Promise<ApiResponse<ApiEndpoints[K]['response']>> {
    const [method, pathTemplate] = (endpoint as string).split(' ');

    // 경로 파라미터 치환
    let path = pathTemplate;
    if (options?.params) {
      for (const [key, value] of Object.entries(options.params)) {
        path = path.replace(`:${key}`, String(value));
      }
    }

    // 쿼리 스트링 생성
    const queryString = options?.query
      ? '?' + new URLSearchParams(options.query as Record<string, string>).toString()
      : '';

    const response = await fetch(`${this.baseUrl}${path}${queryString}`, {
      method,
      headers: { 'Content-Type': 'application/json' },
      body: options?.body ? JSON.stringify(options.body) : undefined
    });

    return response.json();
  }
}

// 사용 예시
const api = new ApiClient('https://api.example.com');

// 타입 안전한 API 호출
async function example() {
  // GET /users
  const users = await api.request('GET /users', {
    query: { page: 1, limit: 10 }
  });
  // users.data는 User[]

  // GET /users/:id
  const user = await api.request('GET /users/:id', {
    params: { id: '123' }
  });
  // user.data는 User

  // POST /users
  const newUser = await api.request('POST /users', {
    body: { name: 'John', email: 'john@test.com' }
  });
  // newUser.data는 User
}

React Props 타입 패턴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import { ComponentProps, PropsWithChildren, ReactNode, HTMLAttributes } from 'react';

// 기본 HTML 요소 Props 확장
interface ButtonProps extends ComponentProps<'button'> {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  loading?: boolean;
}

function Button({ variant = 'primary', size = 'md', loading, children, ...props }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      disabled={loading || props.disabled}
      {...props}
    >
      {loading ? 'Loading...' : children}
    </button>
  );
}

// PropsWithChildren 활용
interface CardProps {
  title: string;
  footer?: ReactNode;
}

function Card({ title, footer, children }: PropsWithChildren<CardProps>) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
      {footer && <div className="card-footer">{footer}</div>}
    </div>
  );
}

// 다형성 컴포넌트 (as prop 패턴)
type PolymorphicProps<E extends React.ElementType, P = {}> = P &
  Omit<ComponentProps<E>, keyof P | 'as'> & {
    as?: E;
  };

interface TextOwnProps {
  color?: 'primary' | 'secondary' | 'muted';
  size?: 'sm' | 'md' | 'lg';
}

type TextProps<E extends React.ElementType = 'span'> = PolymorphicProps<E, TextOwnProps>;

function Text<E extends React.ElementType = 'span'>({
  as,
  color = 'primary',
  size = 'md',
  ...props
}: TextProps<E>) {
  const Component = as || 'span';
  return <Component className={`text-${color} text-${size}`} {...props} />;
}

// 사용 예시
function Example() {
  return (
    <>
      <Text>Default span</Text>
      <Text as="p" color="muted">Paragraph</Text>
      <Text as="h1" size="lg">Heading</Text>
      <Text as="a" href="/about">Link</Text>
    </>
  );
}

상태 관리 타입 (Redux, Zustand)

Redux Toolkit 타입

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import { createSlice, PayloadAction, configureStore } from '@reduxjs/toolkit';

// 상태 타입
interface UserState {
  user: User | null;
  loading: boolean;
  error: string | null;
}

const initialState: UserState = {
  user: null,
  loading: false,
  error: null
};

// 슬라이스 생성
const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    setLoading: (state, action: PayloadAction<boolean>) => {
      state.loading = action.payload;
    },
    setUser: (state, action: PayloadAction<User>) => {
      state.user = action.payload;
      state.error = null;
    },
    setError: (state, action: PayloadAction<string>) => {
      state.error = action.payload;
      state.user = null;
    },
    clearUser: (state) => {
      state.user = null;
      state.error = null;
    }
  }
});

// 스토어 생성
const store = configureStore({
  reducer: {
    user: userSlice.reducer
  }
});

// 타입 추출
type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;

// 타입이 적용된 훅
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';

const useAppDispatch = () => useDispatch<AppDispatch>();
const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

// 사용 예시
function UserComponent() {
  const dispatch = useAppDispatch();
  const user = useAppSelector((state) => state.user.user);
  const loading = useAppSelector((state) => state.user.loading);

  return (
    <div>
      {loading && <div>Loading...</div>}
      {user && <div>{user.name}</div>}
    </div>
  );
}

Zustand 타입

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

// 상태 타입
interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartState {
  items: CartItem[];
  total: number;
  addItem: (item: Omit<CartItem, 'quantity'>) => void;
  removeItem: (id: string) => void;
  updateQuantity: (id: string, quantity: number) => void;
  clearCart: () => void;
}

// 스토어 생성
const useCartStore = create<CartState>()(
  devtools(
    persist(
      (set, get) => ({
        items: [],
        total: 0,

        addItem: (item) =>
          set((state) => {
            const existingItem = state.items.find((i) => i.id === item.id);

            if (existingItem) {
              return {
                items: state.items.map((i) =>
                  i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
                ),
                total: state.total + item.price
              };
            }

            return {
              items: [...state.items, { ...item, quantity: 1 }],
              total: state.total + item.price
            };
          }),

        removeItem: (id) =>
          set((state) => {
            const item = state.items.find((i) => i.id === id);
            if (!item) return state;

            return {
              items: state.items.filter((i) => i.id !== id),
              total: state.total - item.price * item.quantity
            };
          }),

        updateQuantity: (id, quantity) =>
          set((state) => {
            const item = state.items.find((i) => i.id === id);
            if (!item) return state;

            const quantityDiff = quantity - item.quantity;
            return {
              items: state.items.map((i) =>
                i.id === id ? { ...i, quantity } : i
              ),
              total: state.total + item.price * quantityDiff
            };
          }),

        clearCart: () => set({ items: [], total: 0 })
      }),
      { name: 'cart-storage' }
    )
  )
);

// 선택자 (Selector) 타입
const selectItems = (state: CartState) => state.items;
const selectTotal = (state: CartState) => state.total;
const selectItemCount = (state: CartState) =>
  state.items.reduce((sum, item) => sum + item.quantity, 0);

// 사용 예시
function CartComponent() {
  const items = useCartStore(selectItems);
  const total = useCartStore(selectTotal);
  const addItem = useCartStore((state) => state.addItem);

  return (
    <div>
      {items.map((item) => (
        <div key={item.id}>
          {item.name} x {item.quantity}
        </div>
      ))}
      <div>Total: {total}</div>
    </div>
  );
}

폼 라이브러리 타입 (React Hook Form + Zod)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

// Zod 스키마 정의
const signUpSchema = z.object({
  email: z.string().email('올바른 이메일 형식이 아닙니다.'),
  password: z
    .string()
    .min(8, '비밀번호는 8자 이상이어야 합니다.')
    .regex(/[A-Z]/, '대문자를 포함해야 합니다.')
    .regex(/[0-9]/, '숫자를 포함해야 합니다.'),
  confirmPassword: z.string(),
  name: z.string().min(2, '이름은 2자 이상이어야 합니다.'),
  age: z.number().min(18, '18세 이상이어야 합니다.').optional(),
  terms: z.literal(true, {
    errorMap: () => ({ message: '약관에 동의해야 합니다.' })
  })
}).refine((data) => data.password === data.confirmPassword, {
  message: '비밀번호가 일치하지 않습니다.',
  path: ['confirmPassword']
});

// Zod 스키마에서 타입 추론
type SignUpFormData = z.infer<typeof signUpSchema>;

// 폼 컴포넌트
function SignUpForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting }
  } = useForm<SignUpFormData>({
    resolver: zodResolver(signUpSchema),
    defaultValues: {
      email: '',
      password: '',
      confirmPassword: '',
      name: '',
      terms: false
    }
  });

  const onSubmit = async (data: SignUpFormData) => {
    console.log('Form data:', data);
    // API 호출
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input {...register('email')} placeholder="이메일" />
        {errors.email && <span>{errors.email.message}</span>}
      </div>

      <div>
        <input {...register('password')} type="password" placeholder="비밀번호" />
        {errors.password && <span>{errors.password.message}</span>}
      </div>

      <div>
        <input
          {...register('confirmPassword')}
          type="password"
          placeholder="비밀번호 확인"
        />
        {errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}
      </div>

      <div>
        <input {...register('name')} placeholder="이름" />
        {errors.name && <span>{errors.name.message}</span>}
      </div>

      <div>
        <input
          {...register('age', { valueAsNumber: true })}
          type="number"
          placeholder="나이 (선택)"
        />
        {errors.age && <span>{errors.age.message}</span>}
      </div>

      <div>
        <label>
          <input {...register('terms')} type="checkbox" />
          약관에 동의합니다.
        </label>
        {errors.terms && <span>{errors.terms.message}</span>}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '처리 중...' : '회원가입'}
      </button>
    </form>
  );
}

환경 변수 타입 안전하게 사용하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// env.ts
import { z } from 'zod';

// 환경 변수 스키마 정의
const envSchema = z.object({
  // 필수 환경 변수
  NODE_ENV: z.enum(['development', 'production', 'test']),
  API_URL: z.string().url(),
  DATABASE_URL: z.string(),

  // 선택적 환경 변수
  PORT: z.string().transform(Number).default('3000'),
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),

  // API 키
  API_KEY: z.string().min(1),
  SECRET_KEY: z.string().min(32)
});

// 환경 변수 타입
type Env = z.infer<typeof envSchema>;

// 환경 변수 검증 및 파싱
function validateEnv(): Env {
  const result = envSchema.safeParse(process.env);

  if (!result.success) {
    console.error('Invalid environment variables:');
    console.error(result.error.format());
    process.exit(1);
  }

  return result.data;
}

// 환경 변수 내보내기
export const env = validateEnv();

// 사용 예시
// import { env } from './env';
// console.log(env.API_URL);  // 타입 안전!
// console.log(env.PORT);     // number 타입

// Vite 환경에서의 타입 정의
// vite-env.d.ts
interface ImportMetaEnv {
  readonly VITE_API_URL: string;
  readonly VITE_APP_TITLE: string;
  readonly VITE_ANALYTICS_ID?: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

// Next.js 환경에서의 타입 정의 (next.config.js와 함께 사용)
declare namespace NodeJS {
  interface ProcessEnv {
    NODE_ENV: 'development' | 'production' | 'test';
    NEXT_PUBLIC_API_URL: string;
    DATABASE_URL: string;
    SECRET_KEY: string;
  }
}

성능과 컴파일 최적화

satisfies 연산자 활용

satisfies 연산자는 TypeScript 4.9에서 도입되었으며, 타입 체크와 타입 추론의 균형을 맞춥니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// as const와 satisfies 비교
type Colors = Record<string, string>;

// as만 사용: 타입이 Colors로 변환됨
const colors1 = {
  red: '#ff0000',
  green: '#00ff00',
  blue: '#0000ff'
} as Colors;

colors1.red;  // 타입: string (구체적인 값을 잃음)

// satisfies 사용: 타입 검사 + 원래 타입 유지
const colors2 = {
  red: '#ff0000',
  green: '#00ff00',
  blue: '#0000ff'
} satisfies Colors;

colors2.red;  // 타입: '#ff0000' (리터럴 타입 유지)
// colors2.purple;  // Error: 'purple' 속성이 없음

// as const + satisfies 조합
const colors3 = {
  red: '#ff0000',
  green: '#00ff00',
  blue: '#0000ff'
} as const satisfies Colors;

// 실전 예제: 라우트 설정
type Route = {
  path: string;
  component: React.ComponentType;
  exact?: boolean;
};

const routes = [
  { path: '/', component: HomePage, exact: true },
  { path: '/about', component: AboutPage },
  { path: '/contact', component: ContactPage }
] satisfies Route[];

// routes는 여전히 구체적인 타입을 유지하면서 Route[] 제약을 만족

const type parameters (5.0+)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// const 타입 파라미터 없이
function getValues<T extends readonly string[]>(values: T) {
  return values;
}

const values1 = getValues(['a', 'b', 'c']);
// 타입: string[]

// const 타입 파라미터 사용
function getConstValues<const T extends readonly string[]>(values: T) {
  return values;
}

const values2 = getConstValues(['a', 'b', 'c']);
// 타입: readonly ['a', 'b', 'c']

// 실전 예제: 상태 머신
function createMachine<const T extends readonly string[]>(states: T) {
  type State = T[number];

  return {
    states,
    transition(from: State, to: State) {
      console.log(`${from} -> ${to}`);
    }
  };
}

const machine = createMachine(['idle', 'loading', 'success', 'error']);

machine.transition('idle', 'loading');  // OK
// machine.transition('idle', 'unknown');  // Error: 'unknown'이 상태에 없음

타입 복잡도 줄이기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// ❌ 복잡한 조건부 타입 체이닝
type ComplexType<T> = T extends string
  ? T extends `${infer A}.${infer B}`
    ? A extends keyof SomeMap
      ? SomeMap[A] extends { [key: string]: infer V }
        ? V extends string
          ? V
          : never
        : never
      : never
    : T
  : never;

// ✅ 단계별로 분리
type ExtractDomain<T extends string> = T extends `${infer A}.${string}` ? A : T;
type GetMapValue<K extends string> = K extends keyof SomeMap ? SomeMap[K] : never;
type ExtractStringValue<T> = T extends { [key: string]: infer V }
  ? V extends string
    ? V
    : never
  : never;

type SimplifiedType<T extends string> = ExtractStringValue<
  GetMapValue<ExtractDomain<T>>
>;

// 재귀 깊이 제한
type RecursiveType<T, Depth extends number[] = []> =
  Depth['length'] extends 10  // 최대 10단계
    ? T
    : T extends object
    ? { [K in keyof T]: RecursiveType<T[K], [...Depth, 0]> }
    : T;

tsconfig 최적화 옵션

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{
  "compilerOptions": {
    // 타입 안전성 최대화
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noPropertyAccessFromIndexSignature": true,

    // 성능 최적화
    "skipLibCheck": true,
    "incremental": true,
    "tsBuildInfoFile": ".tsbuildinfo",

    // 모듈 해석 최적화
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,

    // 출력 최적화
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,

    // 최신 기능 활성화
    "target": "ES2022",
    "module": "ESNext",
    "lib": ["ES2023", "DOM", "DOM.Iterable"],

    // 경로 별칭
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

주요 옵션 설명

옵션설명
noUncheckedIndexedAccess인덱스 접근 시 undefined 가능성 체크
exactOptionalPropertyTypes선택적 속성에 undefined 명시적 할당 필요
skipLibChecknode_modules 타입 체크 스킵 (빌드 속도 향상)
incremental증분 컴파일 활성화
isolatedModules각 파일을 독립적으로 트랜스파일 가능하게 함

FAQ

Q1: anyunknown의 차이점은 무엇인가요?

A: any는 타입 검사를 완전히 비활성화하고, unknown은 타입 안전한 any입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// any: 모든 연산 허용 (위험)
function processAny(value: any) {
  value.foo();        // OK (런타임 오류 가능)
  value.bar.baz();    // OK (런타임 오류 가능)
  value + 1;          // OK
}

// unknown: 타입 체크 필요
function processUnknown(value: unknown) {
  // value.foo();     // Error: Object is of type 'unknown'

  // 타입 검사 후 안전하게 사용
  if (typeof value === 'string') {
    value.toUpperCase();  // OK
  }

  if (typeof value === 'object' && value !== null && 'foo' in value) {
    (value as { foo: () => void }).foo();  // OK
  }
}

권장사항: any 대신 unknown을 사용하고, 타입 가드로 좁히세요.

Q2: 제네릭 타입 파라미터의 네이밍 컨벤션은?

A: 일반적인 컨벤션:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 단일 문자 (간단한 경우)
function identity<T>(value: T): T { return value; }
function map<K, V>(key: K, value: V) {}

// 의미있는 이름 (복잡한 경우)
interface Repository<Entity, Id = string> {
  findById(id: Id): Promise<Entity | null>;
  save(entity: Entity): Promise<Entity>;
}

// 일반적인 규칙
// T - Type (일반적인 타입)
// K - Key
// V - Value
// E - Element (배열 요소)
// R - Return (반환 타입)
// P - Props (React)
// S - State

Q3: typeinterface는 언제 사용해야 하나요?

A: 일반적인 가이드라인:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// interface: 객체 타입, 확장 가능성이 필요한 경우
interface User {
  id: number;
  name: string;
}

// 선언 병합 가능 (라이브러리 타입 확장에 유용)
interface User {
  email: string;  // 자동으로 병합됨
}

// type: 유니온, 튜플, 복잡한 타입 조합
type ID = string | number;
type Point = [number, number];
type EventHandler = (event: Event) => void;

// 조건부 타입, 매핑된 타입
type Readonly<T> = { readonly [K in keyof T]: T[K] };

실용적 조언: 팀 내 일관성이 가장 중요합니다. 하나를 선택하고 일관되게 사용하세요.

Q4: 타입 단언(as)은 언제 사용해야 하나요?

A: 가능한 피하되, 다음 상황에서는 사용할 수 있습니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. DOM API 사용 시
const input = document.getElementById('input') as HTMLInputElement;

// 2. 외부 라이브러리 타입이 부정확할 때
const data = externalLib.getData() as ExpectedType;

// 3. 테스트에서 부분 모킹
const mockUser = { name: 'Test' } as User;

// ❌ 피해야 할 사용
const data = JSON.parse(json) as User;  // 런타임 검증 없이 위험

// ✅ 대안: 런타임 검증
function isUser(data: unknown): data is User {
  return typeof data === 'object' && data !== null && 'name' in data;
}

const parsed = JSON.parse(json);
if (isUser(parsed)) {
  // parsed는 User 타입
}

Q5: Discriminated Union과 일반 Union의 차이점은?

A: Discriminated Union은 공통 리터럴 속성(판별자)을 가집니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 일반 Union: 타입 좁히기 어려움
type Shape = Circle | Square;

function getArea1(shape: Shape) {
  // 어떤 타입인지 판별하기 어려움
  if ('radius' in shape) {
    return Math.PI * shape.radius ** 2;
  }
  return shape.sideLength ** 2;
}

// Discriminated Union: 판별자로 명확한 분기
interface Circle {
  kind: 'circle';  // 판별자
  radius: number;
}

interface Square {
  kind: 'square';  // 판별자
  sideLength: number;
}

type ShapeDiscriminated = Circle | Square;

function getArea2(shape: ShapeDiscriminated) {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;  // shape는 Circle
    case 'square':
      return shape.sideLength ** 2;  // shape는 Square
  }
}

Q6: infer 키워드는 정확히 어떻게 동작하나요?

A: infer는 조건부 타입에서 타입을 추출하고 이름을 붙입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 기본 패턴: T extends Pattern<infer U> ? U : Default
type ElementType<T> = T extends (infer U)[] ? U : never;

type A = ElementType<string[]>;  // string
type B = ElementType<number[]>;  // number
type C = ElementType<string>;    // never

// 함수 타입에서 추출
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type ParamType<T> = T extends (arg: infer P) => any ? P : never;

// 여러 위치에서 추론
type FirstAndLast<T> = T extends [infer First, ...any[], infer Last]
  ? [First, Last]
  : never;

type FL = FirstAndLast<[1, 2, 3, 4, 5]>;  // [1, 5]

// 동일 타입 변수의 여러 추론 (Union으로 합쳐짐)
type Flatten<T> = T extends { a: infer U; b: infer U } ? U : never;

type F = Flatten<{ a: string; b: number }>;  // string | number

Q7: TypeScript 5.x로 업그레이드할 때 주의할 점은?

A: 주요 주의사항:

  1. Breaking Changes 확인 ```typescript // 5.0: lib.d.ts 변경으로 일부 타입이 변경됨 // 예: PromiseConstructor.resolve의 반환 타입

// 5.4: NoInfer 동작 이해 // 기존에 작동하던 타입 추론이 달라질 수 있음

// 5.6: Nullish/Truthy 체크 강화 // 항상 true/false인 조건문이 오류로 처리됨

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2. **tsconfig 업데이트**
```json
{
  "compilerOptions": {
    // 새로운 모듈 해석 모드
    "moduleResolution": "bundler",

    // 새로운 타겟
    "target": "ES2022",

    // 새로운 lib
    "lib": ["ES2023"]
  }
}
  1. 의존성 호환성 확인 ```bash

    TypeScript 버전 확인

    npx tsc –version

의존성 타입 호환성 체크

npm ls @types/react ```


마치며

TypeScript 5.x는 강력한 타입 시스템과 개발자 경험 개선을 통해 더욱 안전하고 생산적인 개발 환경을 제공합니다. 이 가이드에서 다룬 패턴들을 실무에 적용하면:

  1. 컴파일 타임 오류 감지: 런타임 오류를 크게 줄일 수 있습니다
  2. 코드 자동 완성: IDE의 강력한 지원을 받을 수 있습니다
  3. 리팩토링 안전성: 대규모 코드 변경도 자신있게 수행할 수 있습니다
  4. 문서화: 타입 자체가 코드의 의도를 설명합니다

다음 단계 추천

  1. 프로젝트에 strict 모드 활성화
  2. 커스텀 유틸리티 타입 라이브러리 구축
  3. Zod 같은 런타임 검증 라이브러리와 TypeScript 연동
  4. 타입 테스트 도구(tsd, expect-type) 도입

참고 자료

공식 문서

유용한 리소스

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.