본문 바로가기
Programming/TypeScript

[이펙티브 타입스크립트] 타입시스템 - 1

by 코뮤(commu) 2023. 7. 26.
728x90
반응형

타입 시스템

 

타입스크립트를 설치하면, 아래 두 가지를 실행할 수 있다.

 

1. 타입스크립트 컴파일러(tsc)

2. 단독 실행가능한 타입스크립트 서버(tsserver)

 

tsserver는 우리가 사용하는 편집기에 코드 자동완성, 검색, 리팩터링 등의 기능을 제공해줄 수 있다.

코드의 생산성과 품질을 높이려면, 편집기에서 타입스크립트 언어 서비스를 적극적으로 활용하는 것이 좋다.

 

 

 

타입은 값들의 집합이다

 

모든 변수는 런타임에 각자 고유한 값을 가진다.

하지만 코드가 실행되기 전 타입스크립트가 오류를 체크하는 순간엔 타입을 가진다.

타입을 곧 '할당 가능한 값들의 집합'이라고 생각하면 된다.

 

 

type A = 'A';
type B = 'B';
type Twelve = 12;

type AB = 'A' | 'B';
type AB12 = 'A' | 'B' | 12;

const a: AB = 'A'; // 정상
declare let twelve: AB12;
const back: AB = twelve; // 'AB12' 형식은 'AB'형식에 할당할 수 없습니다.

 

 

interface Person {
	name: string;
}

interface Lifespan {
	birth: Date;
    death?: Date;
}

type PersonSpan = Person & Lifespan;

 

& 연산을 공통 속성을 찾아낸다고 생각하여 PersonSpan 타입은 공집합인가? 하는 생각이 들 수도 있지만,

타입 연산자는 인터페이스의 속성이 아닌 값의 집합에 적용된다.

또한 추가적인 속성을 가지는 값도 여전히 그 타입에 속한다.

따라서 Person 과 Lifespan을 둘 다 가지는 값은 인터섹션 타입에 속하게 된다.

 

인터섹션 타입의 값은 각 타입 내의 속성을 모두 포함하는 것이 일반적인 규칙이다.

 

 

하지만, 두 인터페이스의 유니온에서는 그렇지않다.

(나는 개인적으로 이 부분이 헷갈렸어서, 책에서 보자마자 유레카! 의 느낌이었다.)

 

type K = keyof (Person | Lifespan); // 타입이 never

 

정리하자면 아래와 같은 관계인 거다.

 

keyof (A&B) = (keyof A) | (keyof B)
keyof (A|B) = (keyof A) & (keyof B)

 

 

좀 더 일반적으로 PersonSpan 타입을 선언하는건, extends 를 쓰는 것이다.

 

interface Person {
	name: string;
}

interface PersonSpan extends Person {
	birth: Date;
    death?: Date;
}

 

 

타입 공간과 값 공간의 심벌 구분하기

 

(심벌은 단순하게 이름이라고 생각하면 된다.)

 

 

interface Cylinder {
	radius: number;
    height: number;
}

const Cylinder = (radius: number, height: number) => ({radius, height});

 

 

interface 에서 Cylinder 는 타입으로, const Cylinder 에서는 값으로 쓰인다.

아래 코드를 더 봐보자.

 

function calculateVolume(shape: unknown) {
	if (shape instanceof Cylinder) {
    	shape.radis // '{}' 형식에 'radis' 속성이 없습니다.
    }
}

 

 

instanceof 를 이용해 shape 가 Cylinder 타입인지 체크하려고 했을 텐데,

instanceof 는 자바스크립트의 런타입 연산자이기에 값에 대해 연산을 한다.

따라서 Cylinder 는 타입이 아닌 함수를 참조하는 것이다.

 

 

 

타입 단언 말고 타입 선언 쓰기

 

interface Person {name: string};

const a: Person = {name: 'A'};
const b = {name: 'B'} as Person;

 

타입스크립트에서 변수에 값을 할당하고 타입을 부여하는 방법은 위 두가지이다.

 

결과가 같아보이지만, 첫번째 a: Person 은 변수에 '타입 선언'을 붙여 타입을 명시하고,

두번째는 '타입 단언' 을 수행한다.

 

타입단언을 하면, 타입스크립트가 추론한 타입이 있어도 Person 타입으로 간주한다.

타입스크립트가 오류를 잡아내게 하려면, 타입 단언이 아닌 타입 선언을 쓰는게 올바른 방법이다.

 

+ 화살표 함수에서 타입 선언

 

화살표 함수는 추론된 타입이 모호할 때가 있다. 잘 익혀두자.

 

// Person[] 이 아닌 {name: string;}[]
const people = ['a', 'b', 'c'].map(name => ({name})); 


// 가장 직관적인 방법
const people = ['a', 'b', 'c'].map(name =>{
	const person:Person = {name};
    return person;
}); 


// 위 코드를 좀 더 간결하게 작성하는 법
const people:Person[] = ['a', 'b', 'c'].map((name):Person => ({name}));

 

 

객체 래퍼 타입 피하기

 

자바스크립트에는 객체 외에도 기본형 값들에 대한 일곱가지 타입이 있다.

기본형들은 불변이고, 메서드를 가지지 않는 다는 점이 객체와 구분되는데,

string 의 경우 메서드를 가지고 있는 것 처럼 보인다. (charAt 과 같은 메서드)

 

하지만 charAt은 string의 메서드가 아니고, string을 사용할 때 자바스크립트 내부적으로 많은 동작들이 일어난다.

자바스크립트는 기본형과 객체 타입을 서로 자유롭게 변환한다.

string 기본형에 charAt과 같은 메서드를 사용할 때 자바스크립트는 기본형을 String 객체로 래핑하고 메서드를 호출하고,

마지막에 래핑한 객체를 버린다.

 

 

타입스크립트 객체 래퍼타입은 지양하고, 기본형 타입을 사용하는 것을 추천한다.

(string 은 String에 할당할 수 있지만, String은 string에 할당할 수 없기 때문)

 

 

 

오늘은 이렇게 타입 시스템을 알아보았다.

아직 챕터 2가 끝나지 않았으니, 타입시스템에 대해 알아보는 것은 다음 포스팅에 이어가도록 하겠다.

 

 

 

 

 

 

 

728x90
반응형