이펙티브 타입스크립트 읽고 정리
아이템 1 ~ 5 보러가기
아이템 6 ~ 8 보러가기
아이템 9 ~ 13 보러가기
아이템 14 ~ 16 보러가기
아이템 17 ~ 19 보러가기
아이템 20 ~ 25 보러가기
아이템 26 ~ 31 보러가기
3️⃣2️⃣ 유니온의 인터페이스보다는 인터페이스의 유니온을 사용하기
하나의 인터페이스의 속성에 유니온들의 조합을 사용하는 것 보다는 인터페이스는 하나의 역할만 가지고 인터페이스들의 유니온을 사용하는 것이 의미가 명확해진다.
아래는 3가지 타입의 레이아웃과 페인트를 하나의 Layer 인터페이스에서 유니온으로 구성하고 있다. 아래에서 각 타입별로 맞는 layout과 paint가 존재하고 서로 조합해서 사용되는 경우는 존재하지 않는다고 가정하자.
아래의 코드처럼 속성을 유니온으로 구성하는 경우 코드만 봐서는 특정 조합끼리만 사용하다는 것을 알 수 없다.
interface Layer {
layout: FillLayout | LineLayout | PointLayout;
paint: FillPaint | LinePaint | PointPain;
}
아래와 같이 유효한 타입만 정의하여 정의하는 것이 좋다. 그래야 코드의 도움을 받기가 쉬워지고, 다른 사람이 볼 때도 사용할 때 혼돈을 주지 않는다.
interface FillLayer {
type: 'fill';
layout: FillLayout;
paint: FillPaint;
}
interface LineLayer {
type: 'line';
layout: LineLayout;
paint: LinePaint;
}
interface PointLayer {
type: 'point';
layout: PointLayout;
paint: PointPain;
}
type Layer = FillLayer | LineLayer | PointLayer
아래는 일하는 장소와 이름을 저장하는 인터페이스이다. 이때 직장은 있지만 직장이름이 없을 수는 없다. 반대도 마찬가지이다. 이 경우 직장이 있거나 없거나 하는 경우는 존재할 수 있다.
interface Person {
placeOfJob?: string;
nameOfJob?: string;
}
아래와 같이 하나의 묶으로 표현하는 것이 좋다.
interface Person {
job?: {
place: string;
name: string;
}
}
✨ 유니온 타입을 사용할때 유니온의 인터페이스를 사용하고 있다면, 인터페이스의 유니온을 고려해보자.
3️⃣3️⃣ string 타입보다 더 구체적인 타입 사용하기
string 타입은 가장 많이 사용되는 타입이다. 그리고 범위도 굉장히 광범위하다. 그래서 string을 사용하는 경우에는 어떠한 값이 넘어올지 예측하기 힘들다.
실제로 예측 불가능한 값이라면 string을 사용하는 것이 맞다. 하지만 문자열이지만 제한할 수 있는 범위가 존재한다면 구체적인 문자열 타입으로 정의하는 것이 좋다.
대표적으로 타입에 대한 정의다. 아래와 같이 구체적 타입을 사용하는 경우 타입스크립트의 언어 도움을 받을 수 있다. 그리고 아래 문자열 이외에 값을 사용하는 경우 에러를 발생시켜준다.
type PersonType = 'child' | 'adult';
3️⃣4️⃣ 부정확한 타입보다는 미완성 타입을 사용하기
잘못된 타입을 사용하는 것은 없는 것 보다 못한 경우를 발생시킬 수 있다. 구체적인 타입을 지정하는 경우 잘못된 타입으로 지정되는 경구가 발생할 가능성이 높아진다. 이 경우 오히려 타입스크립트의 도움을 제대로 받지 못하게 될 가능성 또한 높아진다.
✨ 타입 정보를 구체적으로 만드려면, 오류메시지와 자동 완성 기능에 주의를 기울여야 한다.
3️⃣5️⃣ 데이터가 아닌, API와 명세를 보고 타입 만들기
API나 명세가 존재한다면, 직접 데이터를 보고 타입을 작성하는 것 보다는, API나 명세를 통해서 타입을 만드는 것이 다양한 상황에 대한 대처가 가능하다. 데이터는 현재 요청 된 결과에 대해서만 값을 가지고 있기 때문에, 다른 상황에 대해서까지 모두 포함되지 않을 가능성이 높기 때문이다.
3️⃣8️⃣ any 타입은 가능한 한 좁은 범위에서만 사용하기
타입을 명시적으로 사용을 하지 않고, 타입스크립트의 타입추론을 활용한다면 추론 된 타입이 프로젝트 전반에 걸쳐 넓게 퍼지게 된다. 만약 함수의 반환타입이 any이거 해당 값을 프로젝트 전반에 널리 사용하게 되면 any 타입이 전체에 퍼지게 된다.
그래서 이름 방지하기 위해 any 타입은 좁은 범위내에서만 사용해야 한다. 아래와 같이 변수 선언 시에 any 타입을 지정하고 함수의 반환값으로 활용하게 된다면 any가 다른곳까지 널리 퍼지게 된다.
function fn() {
const x: any = fn1();
return x;
}
그래서 함수의 반환타입은 명시적으로 작성하는 것이 오류를 미연에 예방하고 함수의 사용방법을 의도에 맞게 전파하기 쉽다.
3️⃣9️⃣ any를 구체적으로 변형해서 사용하기
any는 모든 값을 수용가능한 타입이다. 그렇기 때문에 사용을 최소화해야하며, 구체적인 타입으로 바꾸는 것이 좋다. 차라리 any 보다는 any[]를 사용하는 것이 좀 더 구체적이고 좋은 타입이다.
4️⃣0️⃣ 함수 안으로 타입 단언문 감추기
타입 단언문은 잘 못 사용하면 오류를 발생시킬 수 있다. 하지만 단언문을 사용해야만 하는 경우도 존재한다. 타입 추론은 최대한 보수적으로 타입을 추론할 수 밖에 없다. 모든 가능성을 염두해두어야 하기 때문이다. 그래서 코드를 잘 알고 있는 개발자가 직접 단언문을 사용해야하는 경우가 발생한다.
이러한 경우에도 함수 내부에서만 타입 단언문을 사용해서 외부에서는 타입을 사용하는데 문제가 없도록 해주어야 한다. 그래야 잘못된 타입을 다른 곳으로 전파되는 것을 막을 수 있다.
4️⃣1️⃣ any의 진화를 이해하기
변수에 타입을 지정하지 않는 경우 암시적으로 any 타입을 가진다.
let value; // any
const list = []; // any[]
변수 선언시 값을 할당하지 않았다가, 이후 값을 할당하게 되면 그 이후에 작성된 값에는 할당된 값의 타입으로 지정된다.
let value; // any
value = '1'; // any
value.length; // string
배열도 마찬가지로 타입을 지정하지 않으면 암시적으로 any[]이 된다. 이후 특정 타입의 값을 추가하면 이후부터는 해당 타입의 배열이 된다.
const list = []; // any[]
list.push(1); // any[]
list[0]; // number[]
위와 같은 형상을 any의 진화라고 한다.
✨ 위와 같이 진화를 시키는 것보다는 미리 명시적으로 타입을 지정하는 것이 훨씐 좋다.
4️⃣2️⃣ 모르는 타입의 값에는 any 대신 unknown을 사용하기
unknown의 두가지 특징은 다음과 같다.
첫 번째로, 어떠한 값이든 unknown 타입에 할당이 가능하다.
function fn(param: unknown) {}
fn(1);
fn('1');
fn(true);
fn({});
fn([]);
두 번째로, unknown 타입은 any 또는 unknown에만 할당 가능하다.
function fn(param: unknown): unknown {
return param;
}
const num: number = fn(1); // 오류 발생
const str: string = fn('1'); // 오류 발생
const bool: boolean = fn(true); // 오류 발생
const array: any[] = fn([]); // 오류 발생
const obj: object = fn({}); // 오류 발생
const un: unknown = fn({}); // 정상
const a: any = fn({}); // 정상
위의 특성때문에 값의 타입을 명확히 알지 못하는 경우라면 any보다는 unknown을 사용하는 것이 좋다. unknown 값을 다른 곳에 할달하려고 하면 타입 에러가 발생하기 때문에 이후 명확한 타입을 할당하도록 강제할 수 있기 때문이다.
parseYAML는 any를 반환하고 있다. 이를 수정할 수 없는 함수라고 가정하자. 이를 직접 사용하는 것보다 안전하게 사용하기 위해 safeParseYAML를 만들었다. 이를 사용하면 좀더 타입 안전하게 사용할 수 있다.
function parseYAML(param: string): any {
return param;
}
function safeParseYAML(yaml: string): unknown {
return parseYAML(yaml);
}
제네릭을 활용해서 아래와 같이 사용해도 유사하게 사용할 수 있다. 제네릭에 타입을 전달하지 않으면 unknown이 된다.
function safeParseYAML<T>(yaml: string): T {
return parseYAML(yaml);
}
✨ 책에서는 이 방법보다는 unknown을 반환하도록 하는 것이 타입스크립트에 적절하다고 하나, 그 이유는 모르겠다.
이는 never와는 정반대이다.
- 어떠한 값이든 never에 할당 불가
- never 타입은 어떠한 타입이든 할당 가능
🙄 정리
- 정확하지 않은 타입은 오히려 더 문제가 될 수 있다. 정확하지 타입으로 정의하기 힘들다면 유연하게 타입을 정의하는 것이 나쁜 것만은 아니다.
- any를 사용하는 것보다는 unknown을 사용하자. 다른 타입에 할당할 때 개발자에게 좀 더 구체적인 타입을 할당하도록 강제할 수 있다.
'Typescript' 카테고리의 다른 글
[TypeScript] 이펙티브 타입스크립트 정리 - 아이템 26 ~ 31 (0) | 2025.06.28 |
---|---|
[TypeScript] 이펙티브 타입스크립트 정리 - 아이템 20 ~ 25 (0) | 2025.06.22 |
[TypeScript] 이펙티브 타입스크립트 정리 - 아이템 17 ~ 19 (0) | 2025.06.15 |
[TypeScript] 이펙티브 타입스크립트 정리 - 아이템 14 ~ 16 (0) | 2025.06.14 |
[TypeScript] 이펙티브 타입스크립트 정리 - 아이템 9 ~ 13 (0) | 2025.06.08 |