[Effective TypeScript] 타입스크립트의 타입시스템(2)

이예슬·2022년 11월 6일
0

Effective TypeScript

목록 보기
3/15

아이템 10. 객체 래퍼 타입 피하기

자바스크립트는 객체 이외에도 string, number, boolean, null, undefined, symbol, bigint의 7가지 기본형 타입이 존재한다. 기본형들은 불변이며 메서드를 가지지 않는다는 점에서 객체와 구분된다.

하지만 string의 경우 charAt 등 메서드를 가지고 있는 것처럼 보인다. 사실 이는 string의 메서드가 아니라 String 래퍼 객체의 메서드이다. 즉 string 기본형에는 메서드가 없는 것이 맞으며 charAt 과 같은 메서드들은 자바스크립트에 정의된 메서드를 가지는 String 객체 타입의 메서드이다. 자바스크립트는 객체타입과 기본형을 서로 자유롭게 변환한다. string 기본형에 charAt과 같은 메서드를 사용할 때 자바스크립트는 기본형을 String 객체로 래핑하고 메서드를 호출한 후 마지막에는 래핑한 객체를 버린다.

null과 undefined를 제외하면 다른 기본형들에도 동일하게 객체 래퍼 타입이 존재한다.

타입스크립트는 기본형과 객체 래퍼 타입을 별도로 모델링하므로 타입을 선언할 때 주의해야 한다 .주로 타입스크립트에서 타입을 선언할 때는 객체 래퍼 타입은 지양하며 기본형 타입을 사용해야 한다.

아이템 11. 잉여 속성 체크의 한계 인지하기

타입 스크립트는 타입이 명시된 변수에 객체 리터럴을 할당할 때 해당 타입의 속성이 있는지를 확인함과 동시에 그 외의 속성은 없는지 확인한다. 그리고 해당 타입에 정의되지 않은 속성이 있을 경우 에러가 발생한다.

이는 잉여 속성 체크의 과정이 수행되었기 때문인데 잉여 속성 체크는 객체 리터럴에서 구조 할당 가능 검사와는 별도의 과정으로 이를 구분할 줄 알아야 한다. 잉여 속성 체크를 이용하면 기본적으로 타입 시스템의 구조적 본질을 해지지 않으면서도 객체 리터럴에 알 수 없는 속성을 허용하지 않음으로써 문제를 방지할 수 있다.

단 이처럼 유용한 잉여 속성 체크에도 한계는 있는데 잉여 속성 체크는 임시 변수를 사용하게 되면 적용되지 않는다.

interface Options {
	title: string;
	darkMode?: boolean;
}

const intermediate = { darkmode: true, title: 'TypeScript!' }; 
const o: Options = intermediate; // OK 

Options에서 선언한 darkMode와 intermediate에서 선언한 darkmode가 서로 다름에도 해당 코드는 에러를 발생시키지 않는다. 첫 번째 줄의 오른쪽은 객체 리터럴이지만 두 번째 줄의 오른쪽은 객체리터럴리 아니기 때문에 잉여 속성 체크가 적용되지 않고 있는 것이다.

이 외에도 타입 단언문을 사용할 때에도 잉여 속성 체크는 적용되지 않는다.

아이템 12. 함수 표현식에 타입 적용하기

자바스크립트와 타입스크립트는 함수 표현신과 선언식을 다르게 인식한다.

타입스크립트에서는 함수 표현식을 사용하는 것이 좋다. 함수의 매개변수부터 반환값까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용할 수 있기 때문이다.

type Calculate = (a: number, b: number) => number 
const add : Calculate = (a, b) => a + b;
const sub : Calculate = (a, b) => a - b;

함수의 매개변수에 타입을 선언하는 것보다 함수 표현식 전체 타입을 정의한느 것이 코드도 간결하고 안전하다. 다른 함수의 시그니처를 참조하려면 typeof fn을 사용하면 된다.

아이템 13. 타입과 인터페이스의 차이점 알기

타입스크립트에서 명명된 타입을 정의하는 방법은 type과 interface를 사용하는 두 가지가 있다.

type Student = {
	name: string; 
	age: number; 
}

interface Student {
	name: string; 
	age: number; 
}

대부분 두 가지 방법 중 원하는 것을 사용해도 된다. 중요한 것은 같은 상황에서 동일한 방법으로 명명된 타입을 정의해 일관성을 유지해야 한다는 것이다. 이를 위해서는 두 방법이 가진 차이를 알고 있어야 한다.

interface는 type을 확장할 수 있고 type은 interface를 확장할 수 있다. 하지만 interface는 union 타입과 같은 복잡한 타입은 확장하지 못한다. 복잡한 타입을 확장하고 싶다면 type과 &를 사용해야 한다.

interface StudentWithGrade extends Student {
	grade: string; 
} 

type StudentWithGrage = Student & { grade : string; };

튜플과 배열 타입도 type 키워드를 이용해 더 간결하게 표현할 수 있다.

type Pair = [number, number] 
type StringList = string[]
type NamedNums = [string, ...number[]]

interface Tuple {
	0: number; 
	1: number; 
	length: 2;
}
const t: Tuple = [10, 20]

interface로 튜플과 비슷하게 구현하면 튜플에서 사용할 수 있는 concat 같은 메서드들을 사용할 수 없다.

type만 장점을 가지고 있는 것이 아니다. interface는 보강(augment)이 가능하다.

interface Student {
	name: string;
	age: number
} 

interface Student {
	grade: string
}

const Tom : Student = {
	name: 'Tom';
	age: 20;
	grade: 'A';
} 

위 코드처럼 속성을 확장하는 것을 선언 병합(declaration merging)이라고 한다.

그렇다면 언제 어떤 방식을 사용하는 것이 좋을까?

복잡한 타입 ? 타입 별칭!

간단한 객체 ? 둘 다 가능!

but 일관성과 보강의 관점에서 고려해야 한다.

아직 스타일이 확립되지 않는 프로젝트의 경우 향후 보강의 가능성을 고려해야 한다. 어떤 API에 대한 타입 선언을 작성해야 한다면 추후 병합이 가능한 interface를 사용하는 것이 좋다. 하지만 프로젝트 내부적으로 사용되는 타입에 선언 병합이 발생하는 것은 잘못된 설계이므로 타입을 사용해야 한다.


<이펙티브 타입스크립트> Dan Vanderkam, 프로그래밍 인사이트 (2021)

profile
꾸준히 열심히!

0개의 댓글