Do's and Don'ts
Do's and Don'ts (이렇게 쓰세요, 이렇게 쓰지 마세요)
선언 파일에서 저지르기 쉬운 실수 중 상당수는 아주 쉽게 피할 수 있어요. 이 문서는 흔한 오류를 짚어주고, 어떻게 발견하고 고치는지 설명합니다. 흔한 실수를 피하는 데 도움이 되니 한 번쯤 정독해 볼 만한 내용이에요.
출처: TypeScript 핸드북
일반 타입 (General Types)
Number, String, Boolean, Symbol, Object
❌ 쓰지 마세요 — Number, String, Boolean, Symbol, Object 타입을 쓰는 일은 절대 없어야 해요. 이 타입들은 기본형(primitive)이 아니라 박싱된(boxed) 객체를 가리키는데, JavaScript 코드에서 제대로 쓰이는 경우가 거의 없거든요.
/* WRONG */
function reverse(s: String): String;
✅ 이렇게 쓰세요 — number, string, boolean, symbol 타입을 사용하세요.
/* OK */
function reverse(s: string): string;
Object 대신에는 비기본형인 object 타입을 사용하세요 (TypeScript 2.2에 추가됨).
제네릭 (Generics)
❌ 쓰지 마세요 — 타입 매개변수(type parameter)를 사용하지 않는 제네릭 타입은 쓰지 마세요. 자세한 내용은 TypeScript FAQ 페이지를 참고하세요.
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(); // oops! meant to do something else
k.doSomething(); // error, but would be OK if the return type had been '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, wat?
✅ 이렇게 쓰세요 — 더 일반적인 시그니처를 더 구체적인 시그니처 뒤에 오도록 정렬하세요.
/* 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;
}
이런 통합(collapsing)은 모든 오버로드의 반환 타입이 같을 때만 수행해야 한다는 점을 주의하세요.
❔ 왜? — 이건 두 가지 이유로 중요해요.
TypeScript는 시그니처 호환성을, 대상 시그니처의 어떤 시그니처가 소스의 인자로 호출될 수 있는지를 보고 해석합니다. 그리고 여분의 인자는 허용됩니다. 예를 들어 이 코드는 시그니처가 선택적 매개변수로 올바르게 작성됐을 때만 버그를 드러냅니다.
function fn(x: (a: string, b: number, c: number) => void) {}
var x: Example;
// When written with overloads, OK -- used first overload
// When written with optionals, correctly an error
fn(x.diff);
두 번째 이유는 소비자가 TypeScript의 "엄격한 null 검사(strict null checking)" 기능을 사용할 때입니다. 지정되지 않은 매개변수는 JavaScript에서 undefined로 나타나기 때문에, 선택적 인자를 가진 함수에 명시적인 undefined를 전달하는 건 보통 괜찮아요. 예를 들어 이 코드는 엄격한 null 검사 아래에서 정상이어야 합니다.
var x: Example;
// When written with overloads, incorrectly an error because of passing 'undefined' to 'string'
// When written with optionals, correctly 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를 선택적으로 만들지 않았다는 점을 주의하세요.
❔ 왜? — 이건 여러분의 함수에 값을 "그대로 통과(passing through)"시키는 사람들에게 중요해요.
function fn(x: string): Moment;
function fn(x: number): Moment;
function fn(x: number | string) {
// When written with separate overloads, incorrectly an error
// When written with union types, correctly OK
return moment().utcOffset(x);
}