← 전체 글로 돌아가기

TypeScript

TypeScript 코드에서 any를 조금씩 걷어낸 방법

any를 한꺼번에 없애려 하면 타입 에러가 한꺼번에 터져서 오히려 멈추게 된다. 실제로 효과가 있었던 접근 방법을 정리했다.

레거시 코드를 TypeScript로 마이그레이션하거나 급하게 짠 코드를 정리할 때 any가 곳곳에 박혀 있는 경우가 있다. 처음에는 한 번에 다 없애려고 했는데, 타입 에러가 수백 개 터져서 오히려 멈추게 됐다. 실제로 효과가 있었던 방식을 정리했다.

any가 어디 있는지 먼저 파악한다

npx tsc --noEmit 2>&1 | grep -c "any"
grep -rn ": any" src/ | wc -l

tsconfig.json에서 "noImplicitAny": true를 켜면 타입 추론이 안 되는 지점에서 에러가 난다. 한꺼번에 켜기 어려우면 ESLint 규칙을 warn으로 설정해서 현황 파악부터 한다.

{
  "rules": {
    "@typescript-eslint/no-explicit-any": "warn"
  }
}

가장 바깥 경계부터 타입을 정한다

API 응답이나 외부 라이브러리에서 오는 값이 any로 내부까지 퍼지는 경우가 많다. 이 값을 받는 진입점에서 타입을 정의하면 내부 코드의 any가 자연스럽게 줄어든다.

// Before
async function getUser(id: string): Promise<any> {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

// After
interface User {
  id: string;
  name: string;
  email: string;
}

async function getUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  return res.json() as Promise<User>;
}

실제 응답 구조를 모를 때는 unknown으로 받고, 사용하는 쪽에서 타입 가드를 추가한다.

unknown과 타입 가드 활용

any 대신 unknown을 쓰면 타입 검사를 강제할 수 있다. unknown은 어떤 값이든 받을 수 있지만, 사용하기 전에 타입을 좁혀야 한다.

function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    typeof (value as { id: unknown }).id === 'string'
  );
}

function processResponse(data: unknown) {
  if (isUser(data)) {
    console.log(data.name); // 여기서 data는 User 타입
  }
}

반복되는 타입 가드 패턴은 함수로 빼두면 재사용할 수 있다.

한 번에 전부 고치려 하지 않는다

PR 하나에 any 수십 개를 한꺼번에 고치면 리뷰하기도 어렵고 실수가 숨기 쉽다. 파일 하나 또는 모듈 하나씩 정리하고 npx tsc --noEmit과 테스트로 검증하면서 진행하는 게 안전하다.

@typescript-eslint/no-explicit-any를 warn으로 켜두고 새로 추가되는 any를 막는 것부터 시작하는 것도 방법이다. 기존 것은 천천히 줄이면서 더 이상 늘어나지 않는 상태를 먼저 만든다.