August 04, 2022
이 아티클은 이펙티브 타입스크립트 - 동작 원리의 이해와 구체적인 조언 62가지
책을 읽고 학습하는 과정에서 내용을 정리하고 있습니다.
흔히 타입스크립트에 대한 설명을 읽다보면 다음과 같은 문장들을 볼 수 있다.
“타입스크립트는 자바스크립트의 상위 집합(superset)이다.“ “타입스크립트는 타입이 정의된 자바스크립트의 상위집합이다.”
이 문장들이 정확히 무슨 의미인지, 타입스크립트와 자바스크립트는 어떤 관계인지, 굉장히 밀접한 관계에 있기 때문에 서로 어떻게 연관되어 있는지 제대로 이해하는 것이 중요하다.
이러한 특성은 기존 코드를 유지하면서 일부분만 타입스크립트 적용이 가능하기 때문에 기존에 존재하는 자바스크립트 코드를 타입스크립트로 마이그레이션 할 때 엄청난 이점이 있다. (마이그레이션은 8장에서 다룸)
⇒ 이는 타입스크립트가 타입을 명시하는 추가적인 문법을 가지기 때문
function greet(who: string) {
console.log('Hello', who);
}
이 코드는 유효한 타입스크립트 프로그램이지만, node에서 실행해보면 오류를 출력한다.
: string
은 타입스크립트에서 쓰이는 타입 구문이기 때문에, 타입 구문을 사용하는 순간 타입스크립트 영역에 들어가게 됨
자바스크립트, 타입스크립트의 관계도
타입스크립트 컴파일러는 타입스크립트 뿐만 아니라 일반 자바스크립트 프로그램에도 유용하다.
const city = 'new york city';
console.log(city.toUppercase());
이 코드를 실행하면 아래 오류 발생
타입스크립트의 타입 체커는 타입 구문을 작성하지 않아도, 자바스크립트 코드의 문제점을 찾아낸다.
→ city변수가 string인걸 알려주지 않아도, 초기값으로 타입을 추론한다. (타입 추론은 아이템 3에서 다룸)
타입 시스템의 목표 중 하나는 런타임에 오류를 발생시킬 코드를 미리 찾아내는 것이다. 타입스크립트가 ‘정적’ 타입 시스템이라는 것은 이런 특징을 말한다. 하지만 타입 체커가 모든 오류를 찾아내지는 않는다.
오류가 발생하지 않지만 의도와 다르게 동작하는 코드 예시
const states = [
{ name: 'Alabama', capital: 'Montgomery' },
{ name: 'Alask', capital: 'Juneau' },
{ name: 'Arizona', capital: 'Phoenix' },
// ...
];
for(const state of states) {
console.log(state.capitol); // 스펠링 실수(capital -> capitol)
}
실행 결과는 undefined가 3번 출력된다. 이는 유효한 자바스크립트(타입스크립트)이며, 어떠한 오류도 없이 실행되지만, for loop 내의 state.capitol
은 의도한 코드가 아니다. 이런 경우 타입 체커는 타입 구문 없이도 오류를 찾아낸다. (추가적으로 해결책 또한 제시해준다)
타입스크립트는는 타입 구문 없이도 오류를 찾아낼 수 있지만, 타입 구문을 추가한다면 더 많은 오류를 찾아낼 수 있다. 타입 구문을 통해 코드의 의도
가 무엇인지 타입스크립트에게 알려줄 수 있기 때문에, 알려준 의도
와 코드 동작
사이에 다른 점을 찾을 수 있다.
const states = [
// 스펠링 실수(capitol -> capital)
{ name: 'Alabama', capitol: 'Montgomery' },
{ name: 'Alask', capitol: 'Juneau' },
{ name: 'Arizona', capitol: 'Phoenix' },
// ...
];
for(const state of states) {
console.log(state.capital);
// ~~~~~~ 'capital' 속성이 ... 형식에 없습니다
// 'capitol'을 사용하시겠습니까? (타입체커의 해결책 제안)
}
타입체커가 제안한 해결책은 잘못됨. 우리가 생각했을 때는 capitol
이 오타이지만, 타입스크립트는 어느쪽이 오타인지 판단하지는 못한다.
⇒ 오류의 원인은 추출할 수는 있지만 항상 정확하지 않다. 그러므로 states 타입
을 명시적으로 선언하여 의도를 분명하게 하는 것이 좋은 코드라고 볼 수 있다.
interface State {
name: string;
capital: string;
}
const states: State = [
{ name: 'Alabama', capitol: 'Montgomery' },
{ name: 'Alask', capitol: 'Juneau' },
{ name: 'Arizona', capitol: 'Phoenix' },
// ...
];
for(const state of states) {
console.log(state.capital);
}
타입 선언을 통해 의도가 명확해졌고, 이제 올바른 해결책을 제시해준다.
이 내용을 정리하면 기존 다이어그램에 새로운 영역(타입 체커를 통과한 타입스크립트 프로그램)을 추가할 수 있다. 평소에 작성하는 타입스크립트 코드가 이 영역에 해당된다.
타입스크립트 타입 시스템은 자바스크립트의 런타임 동작을 ‘모델링’ 한다.
const x = 2 + '3'; // 정상 string 타입
const y = '2' + 3; // 정상 string 타입
런타임 체크를 엄격하게 하는 언어를 사용해봤다면 다음 결과들은 런타임 오류가 될만한 코드이다. 하지만 타입스크립트의 타입 체커는 정상으로 인식함.
반대로 정상 동작하는 코드에 오류를 표시하기도 한다.
const a = null + 8; // js: 8 (정상)
// ts: Object is possibly 'null'.
const b = [] + 12; // js: '12' (정상)
// ts: Operator '+' cannot be applied to types 'never[]' and 'number'
alert("hello", "Typescript"); // js: Hello가 출력 (정상)
// Expected 0-1 arguments, but got 2.
다음 코드는 런타임에서는 동작하는 코드이지만, 타입체커는 문제점을 표시한다.
⇒ 타입스크립트의 타입 시스템이 자바스크립트의 런타임 동작을 모델링 하는 것이 기본 원칙이다. 하지만 단순히 런타임 동작을 모델링하는 것 뿐만 아니라 의도치 않은 이상한 코드가 오류로 이어질 수도 있다는 점까지 고려해야 한다.
언제 자바스크립트 런타임 동작을 그대로 모델링 할지, 또는 추가적인 타입 체크를 할지 분명하지 않다면 과연 타입스크립트를 사용해도 되는지 의문이 들 수 있다. 이에 대한 대답은 본인에게 달렸다. 타입스크립트를 도입한다면 분명 오류가 적은 코드를 작성할 수 있다. 하지만 앞서 살펴본 이상한 코드들(null과 8을 더한다거나, []과 12를 더하거나, 불필요한 매개변수를 추가해서 함수를 호출)를 이상하게 여기지 않거나 당연하게 여긴다면 차라리 타입스크립트를 쓰지 않는 게 낫다.
const names = ['a', 'b'];
console.log(names[2].toUpperCase());
// TypeError: Cannot read property 'toUpperCase' of undefined.
타입 체커는 배열 범위(2개) 내에서 값이 사용될 것이라 가정했지만 실제로는 3번째 index 값이 사용되었다. 근본원인은 타입스크립트가 이해하는 값의 타입과 실제 값에 차이가 있기 때문이다.
⇒ 즉, 타입 시스템이 정적 타입의 정확성을 보장
해줄 것 같지만 그렇지 않다. 애초에 타입 시스템은 그런 목적을 위해 만들어지지 않았다.
정확성을 보장하기 위해서는 Reason이나 Elm 같은 언어를 선택하는 것이 좋다. 하지만 이 언어들은 자바스크립트의 상위 집합이 아니기 때문에 마이그레이션 과정이 훨씬 복잡하다.