타입 시스템
타입스크립트를 설치하면, 아래 두 가지를 실행할 수 있다.
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가 끝나지 않았으니, 타입시스템에 대해 알아보는 것은 다음 포스팅에 이어가도록 하겠다.
'Programming > TypeScript' 카테고리의 다른 글
[이펙티브 타입스크립트] 타입스크립트를 알아보자 (0) | 2023.07.13 |
---|---|
[ NestJS ] 의존성 문제 발생 케이스 기록 (0) | 2022.09.28 |
구조적 서브타이핑 / 잉여 속성 체크 (0) | 2022.09.21 |
[ TypeORM ]Active Record vs Data Mapper (0) | 2022.09.19 |
[ TypeScript ] 제네릭(Generics) (0) | 2022.09.15 |