타입스크립트의 가장 중요한 역할 중 하나는 타입 시스템이다.
타입을 지정함으로써 코드의 안정성이 보장되기 때문이다.
타입스크립트를 설치하면, tsc(타입스크립트 컴파일러)와 tsserver(타입스크립트 서버)를 실행할 수 있다.
tsserver는 '언어 서비스'를 제공하는데 편집기뿐만 아니라 tsserver에도 언어 서비스를 설정해두면 좋다.
편집기를 이용하는 경우 각 변수에 대한 지정된 타입을 확인할 수 있다. typescript는 따로 타입을 지정해주지 않은 경우 알아서 추론해준다. 보통 변수에 마우스 오버를 하면 타입을 확인할 수 있다.
먼저 타입을 지정하기 보다는 타입스크립트가 추론한 타입을 확인하고 본인이 의도한 타입과 다를 경우 추가하는 것이 좋다.
언어 서비스는 라이브러리와 라이브러리 타입 선언을 탐색할 때 도움이 된다. 편집기는 'Go to Definition(정의로 이동)' 옵션을 제공하므로 이를 통해 해당 라이브러리가 정의된 지점으로 이동일 가능하다.
vscode에서는 'ctrl+클릭'으로 이동이 가능한데 vscode 밖에 안 써봐서 다른 편집기는 잘 모르겠다.
정의로 이동 기능을 따라 가다 보면 해당 라이브러리의 구조와 타입을 이해할 수 있다.
number 타입은 어떻게 구성되어 있을까?
42와 37.25는 모두 number로 인식되어야 한다.
만약 타입을 집합으로 생각한다면 number에는 무한대의 숫자가 들어있는 집합일 것이다.
그렇다면 공집합을 의미하는 타입은 뭘까? never이다.
never 타입에는 그 어떤 값도 들어올 수 없다.
그 다음으로 작은 타입은 unit 타입으로도 불리는 literal 타입이다. literal 타입은 아래 예시와 같이 그 값 딱 하나만을 의미하는 타입이다.
type A = "A";
type dohee = "dohee";
그렇다면 DoheeA라는 변수에 A 또는 dohee를 넣고 싶다면 어떤 타입을 써야 할까?
물론 string을 쓰면 해결된다. 하지만 딱 두 개만 들어올 수 있게 만들고 싶다면 어떻게 해야 할까? 아래와 같이 Union 타입을 사용하면 된다.
type DoheeA = "dohee" | "A";
위의 예시를 집합으로 생각해보면 DoheeA라는 집합 안에는 dohee와 A만 들어있는 것이다. 따라서 이 집합 안에 B가 있냐고 묻는다면 당연히 없다고 대답한다.
하지만 literal에 Intersection 타입을 적용하면 never와 동일하다. 왜냐하면 dohee이면서 A인 것은 존재하지 않기 때문이다.
이런 의미에서 단순한 값이 아닌 인터페이스는 조금 이해가 어려울 수 있다.
name을 key로 가지는 Person과 birth를 key로 가지는 LifeSpan이 있다고 가정해보자.
name이 있으면 Person이 될 수 있고, birth가 있으면 LifeSpan이 될 수 있다. 따라서 name과 birth를 둘 다 갖고 있는 PersonLifeSpan은 둘의 교집합이 된다.
반대로 둘의 합집합에는 그 어떠한 key도 있을 수 없다.
이처럼 타입을 집합으로 이해한다면 extends에 대해서 더 잘 이해할 수 있다. A extends B는 A의 요소들이 B에 할당 가능하다는 의미로 A가 B의 부분집합이라는 것을 의미한다.
타입스크립트에서 타입의 중요성이 큰 만큼 어떤 것이 타입을 의미하고 어떤 것이 값을 의미하는지 구분하는 것이 꽤나 중요하다.
이름이 같더라도 속하는 공간에 따라 다른 것을 나타낼 수 있어 혼란을 야기한다.
구분하는 방법 중 하나는 타입스크립트 플레이그라운드(Typescript Playground)를 활용하는 것이다. 이 웹사이트에서는 Typescript 코드를 실시간으로 Javascript 코드로 변환된 결과물을 보여준다. 타입은 변환 과정에서 사라지므로 사라지면 타입, 남아있으면 변수라고 볼 수 있다.
class와 enum은 상황에 따라 타입과 값 두 가지 모두 가능하다.
경험 상 interface는 타입으로만 사용할 수 있다.
이를 구분하는 방법은 책에 아주 자세하게 나오니 책을 참고하길 바란다.
가장 인상깊었던 부분은 destructing 부분인데 개인적으로 함수에 destructing 문법을 애용하는 편이다. typescript로 넘어오면서 구조 분해 할당을 할 때, 타입을 설정할 수 있는 방법이 없을까 고민했다.
typescript의 구조 분해 할당은 다음과 같은 방식을 통해 가능하다.
const newFunc = (
{name, age, dessert}:{name: string, age: number, dessert: string}
) : void => {
console.log(`내 이름은 ${name}! ${dessert}를 사랑하지!`);
console.log(`그렇게 내 나이 ${age}에 생을 마감하였다...`);
}
interface Person { name: string };
const dohee: Person = { name: "dohee" }; // 타입 선언
const heongyu = { name: "heongyu" } as Person; // 타입 단언
타입 선언은 그 값이 선언된 타입임을 명시하는 것이고 타입 단언은 타입스크립트의 추론을 무시하고 그 타입이 옳다고 단언하는 것이다.
타입 선언의 경우 타입체커가 옳은 타입인지 확인해주는 반면 타입 단언에는 타입을 확인해주지 않는다. 따라서 타입 단언을 할 경우 안정성이 저하된다.
화살표 함수를 사용할 때에 제대로 반환값의 타입이 추론되지 않는 경우가 있다. 이런 경우에 무심코 단언문을 쓰게 되는데 변수에 타입을 지정해서 return해주거나 반환값의 타입을 명시하여 해결할 수 있다.
마지막에 사용하는 !는 null이 아님을 단언하는 의미라고 할 수 있다. 이 또한 단언문이기 때문에 null이 올 수 있는 상황에도 타입체커가 제대로 확인해주지 않는다.
물론 타입스크립트의 타입체커보다 타입에 대해서 더 잘 안다면 단언문을 사용할 수 있지만, 가급적이면 타입 선언문을 사용할 것을 권장한다.