TypeScript 3.4 적용하며 마주친 readonly 배열 이슈

문제 상황

팀 프로젝트에 TypeScript를 본격 도입하기로 결정했다. 3월에 출시된 TypeScript 3.4를 적용하던 중 기존 코드에서 예상치 못한 타입 에러가 발생했다.

const ALLOWED_ROLES = ['admin', 'editor', 'viewer'] as const;

function checkRole(role: string): boolean {
  return ALLOWED_ROLES.includes(role); // Error: Argument of type 'string' is not assignable
}

as const로 선언한 배열은 readonly ['admin', 'editor', 'viewer'] 타입이 되는데, includes 메서드가 정확한 리터럴 타입만 받아들였다.

해결 과정

처음엔 타입 단언으로 우회하려 했지만, 이는 타입 안정성을 포기하는 것이었다. 세 가지 접근을 시도했다.

1. 타입 가드 함수 작성

type Role = 'admin' | 'editor' | 'viewer';
const ALLOWED_ROLES: readonly Role[] = ['admin', 'editor', 'viewer'];

function isRole(value: string): value is Role {
  return (ALLOWED_ROLES as readonly string[]).includes(value);
}

2. indexOf 사용

function checkRole(role: string): boolean {
  return ALLOWED_ROLES.indexOf(role as Role) !== -1;
}

3. Set 활용 (최종 선택)

const ALLOWED_ROLES = new Set(['admin', 'editor', 'viewer'] as const);

function checkRole(role: string): boolean {
  return ALLOWED_ROLES.has(role);
}

성능과 가독성을 고려해 Set을 사용하는 방식으로 정리했다. 런타임에서도 더 효율적이고 타입 에러도 발생하지 않았다.

배운 점

TypeScript의 타입 좁히기(type narrowing)를 제대로 이해하지 못하면 예상치 못한 벽에 부딪힌다. as const는 강력하지만 기존 JavaScript 패턴과 충돌할 수 있다는 걸 경험했다. 마이그레이션 과정에서 이런 케이스들을 문서화해두니 팀원들에게도 도움이 됐다.

TypeScript 3.4 적용하며 마주친 readonly 배열 이슈