← 전체 글로 돌아가기

TypeScript

TypeScript에서 optional과 null을 구분해서 타입 에러 없애기

TypeScript의 타입 에러는 복잡해 보이지만, 대부분 optional(?)과 null/undefined를 헷갈렸거나 타입 가드를 빠뜨린 경우다.

혼자 TypeScript 프로젝트를 진행하다 보면 타입 에러가 쌓인다. 특히 "Cannot read property of undefined" 같은 런타임 에러를 방지하려고 타입 체크를 더 엄격하게 하려 할수록 더 많은 에러가 나타난다.

Optional과 Nullable 구분하기

TypeScript에서 가장 자주 헷갈리는 부분이다.

// Optional: 속성이 있을 수도 없을 수도 있음
type User = {
  name: string;
  nickname?: string; // undefined일 수 있음
};

// Nullable: 속성은 있지만 null/undefined 값을 가질 수 있음
type User2 = {
  name: string;
  nickname: string | null; // null일 수 있음
};

두 경우 모두 user.nickname에 접근하려면 체크가 필요하지만, 의미가 다르다. Optional은 속성 자체가 없을 수 있고, Nullable은 속성은 있지만 값이 null일 수 있다는 뜻이다.

타입 가드로 에러 방지하기

값을 사용하기 전에 항상 타입을 확인한다.

// 나쁜 예: 타입 체크 없이 사용
function greet(user: User) {
  console.log(user.nickname.toUpperCase()); // nickname이 undefined일 수 있음
}

// 좋은 예: 타입 가드
function greet(user: User) {
  if (user.nickname) {
    console.log(user.nickname.toUpperCase());
  }
}

// 더 명시적인 예
function greet(user: User) {
  if (user.nickname && user.nickname !== null) {
    console.log(user.nickname.toUpperCase());
  }
}

타입 가드 함수 만들기

자주 나타나는 패턴은 타입 가드 함수로 따로 빼면 코드가 깔끔해진다.

// 배열에서 null이나 undefined를 제거
function isDefined<T>(value: T | undefined | null): value is T {
  return value !== undefined && value !== null;
}

const items = [1, null, 2, undefined, 3];
const validItems = items.filter(isDefined); // [1, 2, 3]

빌드 에러 메시지 읽기

타입 에러 메시지는 길지만 실제로는 중요한 정보가 담겨있다.

Type '{ name: string; }' is not assignable to type '{ name: string; email: string; }'.
  Property 'email' is missing in type '{ name: string; }'

이 메시지는 "email 속성이 필수인데 전달한 객체에 없다"는 뜻이다. 전달하는 객체에 email을 추가하거나, 타입을 변경해야 한다.

제네릭과 extends 활용

복잡한 타입을 다룰 때는 제네릭으로 보다 정확한 타입을 표현할 수 있다.

// 제네릭을 사용하지 않은 경우
function process(data: any): any {
  return data;
}

// 제네릭으로 입출력 타입을 명시
function process<T>(data: T): T {
  return data;
}

// API 응답 처리
interface ApiResponse<T> {
  status: number;
  data: T;
}

function fetchUser(id: number): Promise<ApiResponse<User>> {
  // 구현
}

Strict 모드 단계적으로 활성화

프로젝트 시작부터 엄격한 설정을 할 수 없다면 점진적으로 활성화한다.

// tsconfig.json
{
  "compilerOptions": {
    "strict": false,
    "strictNullChecks": true, // null/undefined 체크만 먼저
    "noImplicitAny": true // any 타입 명시 요구
  }
}

테스트와 타입 확인

타입이 정확한지 확인하려면 테스트를 작성한다.

test('User nickname should be optional', () => {
  const user: User = { name: 'John' }; // nickname 없음
  const user2: User = { name: 'Jane', nickname: 'j' }; // nickname 있음

  // 둘 다 유효한 User
  expect(user).toBeDefined();
  expect(user2).toBeDefined();
});

체크리스트

  1. 타입 정의에서 optional(?)과 nullable (| null)을 구분한다.
  2. 값을 사용하기 전에 항상 타입 가드로 확인한다.
  3. 자주 쓰는 가드 패턴은 함수로 따로 만든다.
  4. 타입 에러 메시지를 끝까지 읽고 정확히 이해한다.
  5. 제네릭으로 타입을 더 정확하게 표현한다.
  6. 점진적으로 strict 설정을 활성화한다.

TypeScript 타입 에러는 번거로워 보이지만, 실제로는 런타임 에러를 사전에 방지하는 소중한 신호다.