Menu

TypeScript 베스트 프랙티스: 전후 비교로 보는 8가지 규칙

실제 버그를 막아 주는 TypeScript 습관 8가지: strict 유지하기, any 대신 unknown 쓰기, 추론에 맡기기, enum보다 유니언, satisfies로 설정 검사하기, 판별 유니언으로 상태 모델링하기, !와 as 피하기, 데이터를 readonly로 만들기. 각각 실행 가능한 전후 비교 예제가 있습니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

아래 규칙들은 실제 TypeScript 코드에서 가장 많은 버그를 막아 주는 것들입니다. 각각 흔히 쓰는 방식을 먼저, 더 나은 방식을 다음에 실행 가능한 코드로 보여 줍니다. 가장 중요한 것은 첫 번째 규칙, any를 그만 쓰는 것입니다.

any 대신 unknown 쓰기

any는 값과 그 값에서 계산된 모든 것의 타입 검사를 끕니다. unknown도 어떤 값이든 받지만 사용하기 전에 검사해야 하므로, 데이터가 프로그램에 들어오는 지점에 검사를 두게 됩니다.

경계는 타입이 더 이상 보장되지 않는 곳입니다: JSON.parse, fetch 응답, localStorage, 폼 입력, 환경 변수, 다른 프로세스에서 온 메시지. 그곳에서 타입 가드나 스키마 라이브러리로 검증하면 나머지 코드는 타입을 믿을 수 있습니다.

strict 유지하기

strict는 TypeScript 7의 기본값입니다. 끄지 마세요. 그리고 strict가 포함하지 않는 옵션 중 버그를 가장 많이 잡는 것들을 추가하세요.

{
    "compilerOptions": {
        "strict": true,
        "noUncheckedIndexedAccess": true,
        "noImplicitOverride": true,
        "noFallthroughCasesInSwitch": true
    }
}

noUncheckedIndexedAccess는 arr[i]와 record[key]에 undefined를 포함시키는데, 없는 인덱스에서 실제로 반환되는 값이 바로 그것입니다. noImplicitOverride는 하위 클래스 메서드에 override를 쓰게 하고, noFallthroughCasesInSwitch는 다음 case로 넘어가는 case를 거부합니다.

추론에 맡기기

TypeScript가 알 수 없는 것에만 타입을 표기하세요. 함수 매개변수와, 다른 모듈이 쓰는 함수의 반환 타입입니다. 지역 변수와 콜백 매개변수는 추론에 맡기세요. 불필요한 타입 표기는 군더더기일 뿐 아니라 타입을 값보다 넓게 만들 수도 있습니다.

@ts-expect-error 주석이 없으면 setStatus(annotated)는 TS2345 오류입니다. 추론된 const는 리터럴 타입 "active"를 유지하므로 받아들여집니다. 타입을 추가하기 전에 에디터에서 변수 위에 마우스를 올려 무엇이 추론됐는지 확인하세요.

enum보다 유니언 타입

문자열 리터럴 유니언은 생성 코드 없이 자동 완성과 전수 검사를 제공합니다. 런타임에 값 목록도 필요하다면 as const 배열에서 타입을 파생시키세요.

const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"

function canEdit(role: Role): boolean {
  return role !== "viewer";
}

console.log(canEdit("editor"));     // true
console.log(ROLES.filter(canEdit)); // [ 'admin', 'editor' ]

canEdit("owner"); // error TS2345: Argument of type '"owner"' is not assignable to parameter of type '"admin" | "editor" | "viewer"'.

enum Role { Admin, Viewer }는 역방향 매핑을 가진 객체로 컴파일되고, 숫자 enum 매개변수는 enum에 없는 값을 담은 number 변수까지 모두 받습니다. enum은 Node의 타입 제거로 실행할 수도 없습니다(TypeScript enum is not supported in strip-only mode). 장단점 비교는 enum 페이지에 있습니다.

satisfies로 설정 객체 검사하기

Record<string, Route>처럼 넓은 타입으로 객체에 타입을 표기하면 값은 검사되지만 키는 잊힙니다. satisfies는 같은 것을 검사하면서 정확한 타입을 유지합니다.

변수가 선언한 타입을 정확히 가져야 할 때(함수 매개변수, 다시 대입할 값)는 타입 표기를 쓰세요. 조회 테이블, 라우트 맵, 테마 토큰 같은 상수 객체에는 satisfies를 쓰세요.

판별 유니언으로 상태 모델링하기

선택적 필드를 가진 객체 하나는 일어날 수 없는 상태를 허용합니다. loading: true이면서 error가 있거나, 성공했는데 data가 없는 상태입니다. 공통 태그를 가진 객체들의 유니언은 실제 상태만 허용하며, 각 분기는 자신의 필드만 봅니다.

never 줄이 전수 검사입니다. 누군가 상태를 추가하고 처리하는 것을 잊으면 그 줄에서 빌드가 깨집니다.

오류는 index.ts(14,13): error TS2322: Type '{ status: "cancelled"; }' is not assignable to type 'never'.입니다. 처리하지 않은 case의 이름을 알려 줍니다. case "cancelled":를 추가하면 컴파일됩니다.

!와 as 피하기

non-null 단언 x!와 타입 단언 x as T는 컴파일러에게 검사를 멈추라고 말합니다. 둘 다 런타임에 값을 바꾸지 않으므로, 잘못된 단언은 나중에 원인에서 멀리 떨어진 곳에서 멈춤으로 나타납니다.

!는 ?., ??, 이른 return, 또는 유용한 메시지를 담은 예외로 바꾸세요. as는 값을 실제로 검사하는 타입 가드로 바꾸세요. 항상 안전한 단언은 as const 하나뿐입니다. 타입을 더 좁고 읽기 전용으로 만들기만 하기 때문입니다. 좋은 린트 설정(typescript-eslint의 no-non-null-assertion과 no-explicit-any)이 나머지를 표시해 줍니다.

데이터를 readonly로 만들기

코드가 바꾸면 안 되는 프로퍼티와 배열에는 readonly를 표시하세요. 그러면 컴파일러가 push, sort, 대입을 거부하고, 함수는 입력을 변경하는 대신 새 값을 반환하게 됩니다.

readonly는 컴파일 시점에, 그것도 한 단계 깊이까지만 검사됩니다. 런타임에 객체를 동결하지는 않습니다. 그래도 이런 버그 대부분의 원인인, 공유 상태를 실수로 변경하는 일을 잡아내기에는 충분합니다.

자주 묻는 질문

TypeScript에서 any를 써도 되나요?

애플리케이션 코드에서는 거의 쓰지 않아야 합니다. any는 그 값과 거기서 파생된 모든 것의 검사를 끕니다. 아직 타입을 모르는 값에는 unknown을 쓰고 검사로 좁히세요. any는 드물게 필요한 탈출구로만 남겨 두고, 이유를 주석으로 적으세요.

TypeScript에서 모든 변수에 타입을 표기해야 하나요?

아니요. 지역 변수와 콜백 매개변수는 TypeScript가 추론하게 두세요. 함수 매개변수(추론할 수 없습니다)와 export한 함수의 반환 타입에는 표기하세요. 그래야 함수 내부를 바꿨을 때 공개 타입이 조용히 바뀌지 않습니다.

TypeScript enum은 나쁜 관행인가요?

틀린 것은 아니지만 많은 팀이 피합니다. enum은 런타임 코드를 생성하고, Node의 타입 제거(type stripping)에서 실행되지 않으며, 숫자 enum 매개변수는 어떤 값이 들었든 모든 number 변수를 받습니다. 문자열 리터럴 유니언이나, 타입을 파생시킨 as const 배열은 생성 코드 없이 같은 자동 완성과 검사를 제공합니다.

as를 이용한 타입 단언은 언제 써야 하나요?

컴파일러가 알 수 없는 것을 여러분이 알 때만, 가능하면 그것을 증명하는 검사 바로 뒤에서 쓰세요. as는 런타임에 값을 전혀 바꾸지 않으므로 {} as User는 컴파일되지만 name이 없습니다. 대부분의 경우 값을 실제로 검사하는 타입 가드가 더 안전합니다.

새 TypeScript 프로젝트에 가장 좋은 tsconfig 설정은 무엇인가요?

strict를 켜 두고(TypeScript 7의 기본값) noUncheckedIndexedAccess를 추가하세요. 많은 프로젝트가 noImplicitOverride, noFallthroughCasesInSwitch, verbatimModuleSyntax도 켭니다. tsc --init이 쓰는 설정에는 strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes, verbatimModuleSyntax 등이 들어 있습니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기