.d.ts 파일을 작성할 때 이렇게 하세요 — Do's and Don'ts
.d.ts 파일을 작성할 때 이렇게 하세요 — Do's and Don'ts
타입 선언 파일(.d.ts)을 작성할 때 어떤 습관이 좋고 나쁜지 정리해 둔 문서예요. 항목마다 "이렇게 하면 안 돼요"와 "이렇게 하세요"를 나란히 두고, 왜 그래야 하는지까지 짚어 주니까 실전에서 바로 적용할 수 있는 규칙이에요. 코드와 타입 시그니처는 원문 그대로 두고, 설명하는 말투만 옆에서 알려 주는 투로 바꿨어요.
출처: TypeScript 공식문서
본문
일반 타입 (General Types)
Number, String, Boolean, Symbol 그리고 Object
이렇게 하면 안 돼요 — Number, String, Boolean, Symbol, Object 타입은 절대 쓰지 마세요. 이 타입들은 원시값이 아니라 박싱(boxed)된 객체 타입을 가리키는데, JavaScript 코드에서 이런 객체 타입은 거의 올바르게 쓰이는 경우가 없거든요.
/* WRONG */
function reverse(s: String): String;
이렇게 하세요 — 소문자 타입 number, string, boolean, symbol을 쓰세요.
/* OK */
function reverse(s: string): string;
Object가 필요하다면, TypeScript 2.2에서 추가된 비원시 object 타입을 대신 쓰세요.
Generics
이렇게 하면 안 돼요 — 타입 매개변수를 전혀 사용하지 않는 제네릭 타입은 만들지 마세요. 자세한 내용은 TypeScript FAQ page에서 확인할 수 있어요.
any
이렇게 하면 안 돼요 — JavaScript 프로젝트를 TypeScript로 마이그레이션하는 중이 아니라면 any를 타입으로 쓰지 마세요. 컴파일러는 any를 "이 녀석은 타입 체크를 꺼 줘"라는 뜻으로 취급해요. 변수를 쓸 때마다 @ts-ignore 주석을 하나씩 붙여 주는 것과 비슷하다고 보면 돼요. 처음 JavaScript 프로젝트를 TypeScript로 옮길 때는 아직 옮기지 못한 부분의 타입을 any로 둘 수 있어서 아주 유용하지만, 완전한 TypeScript 프로젝트에서는 그 부분들의 타입 체크를 전부 끈 셈이라 문제가 생겨요.
무엇을 받아야 할지 모를 때, 또는 그냥 통과만 시킬 거라 아무거나 받아도 될 때는 unknown을 쓰면 돼요.
콜백 타입 (Callback Types)
콜백의 반환 타입 (Return Types of Callbacks)
이렇게 하면 안 돼요 — 반환값을 무시할 콜백의 반환 타입으로 any를 쓰지 마세요:
/* WRONG */
function fn(x: () => any) {
x();
}
이렇게 하세요 — 반환값을 무시할 콜백의 반환 타입으로는 void를 쓰세요:
/* OK */
function fn(x: () => void) {
x();
}
왜 그럴까요? void를 쓰면 x()의 반환값을 실수로 검사 없이 사용하는 일을 막을 수 있어서 더 안전해요:
function fn(x: () => void) {
var k = x(); // 어이쿠, 다른 걸 하려던 참이었는데
k.doSomething(); // 에러, 하지만 반환 타입이 'any'였다면 통과했을 거예요
}
콜백의 선택적 매개변수 (Optional Parameters in Callbacks)
이렇게 하면 안 돼요 — 진짜 의도한 게 아니라면 콜백에 선택적 매개변수를 쓰지 마세요:
/* WRONG */
interface Fetcher {
getObject(done: (data: unknown, elapsedTime?: number) => void): void;
}
이건 뜻이 아주 구체적이에요. done 콜백이 인자 1개로 호출될 수도, 2개로 호출될 수도 있다는 의미거든요. 작성자는 아마 "콜백이 elapsedTime 매개변수를 신경 쓰지 않아도 된다"고 말하고 싶었을 거예요. 그런데 이걸 하려고 매개변수를 선택적으로 만들 필요는 없어요. 더 적은 인자를 받는 콜백을 넘기는 건 언제나 합법이거든요.
이렇게 하세요 — 콜백 매개변수는 선택적이지 않게 쓰세요:
/* OK */
interface Fetcher {
getObject(done: (data: unknown, elapsedTime: number) => void): void;
}
오버로드와 콜백 (Overloads and Callbacks)
이렇게 하면 안 돼요 — 콜백의 인자 개수(arity)만 다른 오버로드를 따로따로 나눠 쓰지 마세요:
/* WRONG */
declare function beforeAll(action: () => void, timeout?: number): void;
declare function beforeAll(
action: (done: DoneFn) => void,
timeout?: number
): void;
이렇게 하세요 — 최대 인자 개수를 쓰는 오버로드 하나로만 작성하세요:
/* OK */
declare function beforeAll(
action: (done: DoneFn) => void,
timeout?: number
): void;
왜 그럴까요? 콜백이 매개변수 하나를 무시하는 건 언제나 합법이라, 더 짧은 오버로드가 필요 없어요. 더 짧은 콜백을 앞에 두면 잘못된 타입의 함수도 첫 번째 오버로드에 부합하기 때문에 그대로 통과해 버릴 수 있어요.
함수 오버로드 (Function Overloads)
순서 (Ordering)
이렇게 하면 안 돼요 — 더 일반적인 오버로드를 더 구체적인 오버로드보다 앞에 두지 마세요:
/* WRONG */
declare function fn(x: unknown): unknown;
declare function fn(x: HTMLElement): number;
declare function fn(x: HTMLDivElement): string;
var myElem: HTMLDivElement;
var x = fn(myElem); // x: unknown, 어?
이렇게 하세요 — 구체적인 시그니처를 앞에, 일반적인 시그니처는 뒤에 두도록 정렬하세요:
/* OK */
declare function fn(x: HTMLDivElement): string;
declare function fn(x: HTMLElement): number;
declare function fn(x: unknown): unknown;
var myElem: HTMLDivElement;
var x = fn(myElem); // x: string, :)
왜 그럴까요? TypeScript는 함수 호출을 풀 때 첫 번째로 부합하는 오버로드를 골라요. 앞에 있는 오버로드가 뒤에 것보다 "더 일반적"이면, 뒤의 오버로드는 사실상 숨겨져서 호출할 수 없게 돼요.
선택적 매개변수 쓰기 (Use Optional Parameters)
이렇게 하면 안 돼요 — 뒤쪽 매개변수만 다른 오버로드를 여러 개 나눠 쓰지 마세요:
/* WRONG */
interface Example {
diff(one: string): number;
diff(one: string, two: string): number;
diff(one: string, two: string, three: boolean): number;
}
이렇게 하세요 — 가능하면 선택적 매개변수를 쓰세요:
/* OK */
interface Example {
diff(one: string, two?: string, three?: boolean): number;
}
이렇게 합치는 건 모든 오버로드의 반환 타입이 같을 때만 해야 한다는 점을 기억하세요.
왜 그럴까요? 여기에는 두 가지 중요한 이유가 있어요.
TypeScript는 시그니처 호환성을 확인할 때, 대상 시그니처의 어느 것이든 출처의 인자로 호출될 수 있는지 보는데, 여분의 인자는 허용돼요. 예를 들어 아래 코드는 시그니처를 선택적 매개변수로 바르게 작성했을 때만 버그가 드러나요:
function fn(x: (a: string, b: number, c: number) => void) {}
var x: Example;
// 오버로드로 썼을 때: OK, 첫 번째 오버로드를 사용
// 선택적 매개변수로 썼을 때: 바르게 에러
fn(x.diff);
두 번째 이유는, 사용자가 TypeScript의 "엄격한 null 검사" 기능을 쓸 때예요. 지정되지 않은 매개변수는 JavaScript에서 undefined로 나타나기 때문에, 선택적 인자를 가진 함수에 명시적으로 undefined를 넘기는 건 보통 괜찮아요. 예를 들어 아래 코드는 엄격한 null 검사에서 OK여야 해요:
var x: Example;
// 오버로드로 썼을 때: 'undefined'를 'string'에 넘기므로 잘못된 에러
// 선택적 매개변수로 썼을 때: 바르게 OK
x.diff("something", true ? undefined : "hour");
유니언 타입 쓰기 (Use Union Types)
이렇게 하면 안 돼요 — 한 인자 위치의 타입만 다른 오버로드를 나눠 쓰지 마세요:
/* WRONG */
interface Moment {
utcOffset(): number;
utcOffset(b: number): Moment;
utcOffset(b: string): Moment;
}
이렇게 하세요 — 가능하면 유니언 타입을 쓰세요:
/* OK */
interface Moment {
utcOffset(): number;
utcOffset(b: number | string): Moment;
}
여기서 b를 선택적으로 만들지 않은 건, 시그니처들의 반환 타입이 서로 다르기 때문이에요.
왜 그럴까요? 이건 "값을 그대로 통과"시키는 사람에게 중요해요:
function fn(x: string): Moment;
function fn(x: number): Moment;
function fn(x: number | string) {
// 별도 오버로드로 썼을 때: 잘못된 에러
// 유니언 타입으로 썼을 때: 바르게 OK
return moment().utcOffset(x);
}
더 알아보기
- 다음 단계로는 Deep Dive를 읽어 보세요.
.d.ts파일이 실제로 어떻게 동작하는지 깊이 파고드는 내용이에요. - 이 문서의 원문은 TypeScript-Website 저장소의 Do's and Don'ts.md에서 확인할 수 있고, Pull Request로 개선에 참여할 수도 있어요.