이펙티브 타입스크립트를 읽고 개인적으로 정리한 내용을 기록하는 글입니다.
타입스크립트는 자바스크립트의 상위 집합 (Superset)이다
는 정확히 무슨 의미인가.
타입스크립트는 문법적으로 자바스크립트의 상위 집합이다.
자바스크립트 프로그램에 문법 오류가 없으면 유효한 타입스크립트 프로그램이다.
자바스크립트 프로그램에 문법 오류가 아니더라도 타입 체커에 지적당할 수 있으나 문법의 유효성과 동작의 이슈는 독립적인 문제이다.
js 파일에 있는 코드는 이미 타입스크립트라고 할 수 있다. main.js 파일을 main.ts로 바꾼다고 할지라도 프로그램은 정상으로 돌아간다.
아래 명제는 참이다.
모든 자바스크립트 프로그램은 타입스크립트이다 (참)
모든 타입스크립트 프로그램은 자바스크립트이다. (거짓)
타입스크립트는 별도의 문법을 가지고 있기 때문에 컴파일 과정을 거치지 않으면 자바스크립트 구동환경에서 유효하지 않을 수 있다.
다음 코드는 유효한 타입스크립트 프로그램이나 자바스크립트 구동환경에서는 오류를 출력한다.
function hello(who:string) {
console.log(`hello ${who}!`);
}
js 확장자 파일을 ts로 바꾸기만 하더라도 타입스크립트 컴파일러는 아래와 같은 오류를 잡아내는데 용이하다.
const name = "dante"
console.log (name.toUppercase())
// TypeError name.toUppercase is not a function
타입 시스템의 목표 중 하나는 위와 같은 런타임 오류를 미연에 방지하는 것이다.
이렇게 런타임 전의 오류를 잡아내주는 것은 정적 시스템의 특성이다.
앞서 말한 것과 같이, 타입 체커를 통과하지 않은 자바스크립트 프로그램 또한 런타임에서 정상적으로 돌아가기 때문에, 타입스크립트 프로그램과 자바스크립트 프로그램간의 관계는 아래와 같다.
타입스크립트 프로그램
= 자바스크립트 프로그램 U 타입 체커를 통과한 자바스크립트 프로그램
(여기서 U는 합집합)
각종 블로그글들을 통해 작성자의 개성이 들어나는 타입스크립트 프로젝트 설정을 단순히 따라하다보면 어떤 설정들이 타입스크립트에서 가능한지 자세히 알지 못하는 경우가 많다.
웹팩 설정을 따라하는 당신은 @babel/core와 @babel/preset-env의 역할에 대해 잘 설명할 수 있는가?
tsconfig.json 이라고 하는 파일에는 수 많은 옵션이 존재하고, 이러한 옵션을 통해 타입 체커의 오류표시에 대한 범위를 설정해줄 수 있다.
다음 코드는 타입스크립트 설정 파일에 따라 타입 체커를 통과할 수도, 통과하지 못할 수도 있다.
function add(a, b) {
return a + b;
}
add(10, null);
noImplicitAny
옵션은 변수들이 미리 정의된 타입을 가져야 하는지에 대한 여부를 제어한다.
만약 다음의 코드처럼 작성된 함수가 있다면 함수 인자 a,b는 암시적으로 any 타입을 갖게 된다.
function add(a, b): any
// Parameter 'a' implicitly has an 'any' type.ts(7006)
// Parameter 'b' implicitly has an 'any' type.ts(7006)
strictNullChecks
는 null과 undefined를 모든 타입에서 혀용할지의 여부를 알려준다.
let c: number;
c = null;
// Type 'null' is not assignable to type 'number'.ts(2322)
tsconfig.json 파일을 이용해 100가지가 넘는 타입 체커 설정을 할 수 있다.
다 외우기는 힘들지만, 프로젝트에서 왜 이런 설정을 사용하는지는 한번 알아보자.
중요한 설명이 이어진다.
타입스크립트 컴파일러는 두가지 역할을 한다.
다음 두가지는 서로 독립적으로 일어난다.
트랜스파일
한다.트랜스파일
이라는 단어가 등장한다. 트랜스파일은 translate + compile의 합성어이자 신조어다. 코드를 동일한 동작을 하는 다른 버전으로 단순히 변환하기 때문에 아직 컴파일이 되지 않았다. 따라서 컴파일과 구분해서 부른다는 것이다.
타입체크와 컴파일이 독립적으로 일어난다는 것은 아이템1
에서 이미 드러났다.
타입 오류가 있어도 타입스크립트 컴파일 (트랜스파일)이 가능하다.
내가 개인 프로젝트를 진행할때 잦은 타입 에러가 빌드 결과에 나와 고개를 갸우뚱한 적이 있다. 사실상 런타임에서는 문제가 없으니까 이상하다고 느낀 것이다. tsconfig.json에 noEmitOnError
를 설정하면 타입 오류와 무관하게 애플리케이션을 테스트할 수 있다.
테스트 코드를 작성할때 instanceof 를 사용한 경험이 있다. 모델 테스트에 해당 모델이 잘 생성되었는지를 확인하기 위함이었다. 자연스럽게 사용한 문법에도 사실 더 세부적인 상황들이 숨겨져있었다.
interface Box {
size: string;
}
interface GiftBox extends Box {
color: green;
}
if(someBox instanceof GiftBox){
...
}
...
예제 코드의 instanceof 체크는 런타임에 일어나지만 GiftBox는 타입 정보이기에 트랜스파일을 거치며 런타임에서 없어진다. 따라서 런타임 체크문에 타입을 사용할 수 없다.
이 경우 다음과 같이 속성 체크를 하거나
if( "color" in someBox)
태그 기법을 사용한다.
이 방법은 생소할 수 있지만
또 다른 인터페이스와 GiftBox가 속성이 동일할때 사용하면 좋을 것 같다.
interface Box {
kind: "box"
...
}
interface GiftBox {
kind: "gift"
...
}
if(someBox.kind === "gift")
앞서 테스트 대상이었던 모델처럼 클래스의 경우 타입과 값으로 같이 사용할 수 있다.
타입과 타입 연산자는 트랜스파일을 거치면 제거되기 때문에 런타임 성능에 영향을 주지 않는다. 타입스크립트가 제공하는 정적타입은 실제로 비용이 전혀 들지 않는다.
런타임 오버헤드가 없는데신 타입스크립트 컴파일러는 빌드타임 오버헤드가 있다. 하지만 런타임 성능에 영향을 미치지 않는다는 사실이 새삼 놀랍지 않은가?
다음의 예제에서 coord
는 Vector3D의 속성을 가지고 있음에도 calculateLength
의 파라메터로 넣을 수 있다.
interface Vector2D {
x: number;
y: number;
}
interface Vector3D extends Vector2D {
z: number;
}
function calculateLength(v: vector2D) {
return Math.sqrt(v.x * v.x + v.y * v.y);
}
const coord = { x: 1, y:2, z:4}
calculateLength(coord)
타입스크립트는 매개변수 v를 구조적 타이핑 관점에서 x,y가 있기에 Vector2D와 호환된다고 판단한다. 따라서 타입체커가 이를 오류로 문제삼지 않는다.
함수 매개변수는 개발자의 의도와는 다르게 타입 확장을 허용하게 되며 이를 열려있다고 (open) 표현한다.
귀여운 강아지든 사나운 강아지든 모두 동일한 강아지로 받아들인다는 것이다.
이러한 구조적 타이핑이 문제가 되는 경우를 알아보자. 다음 코드를 보자.
function getVector3D (v: Vector3D) {
for( const axis of Object.keys(v)) {
const value = v[axis];
// >>>> 타입 체커 에러 발생
}
}
/**
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Vector3D'.
No index signature with a parameter of type 'string' was found on type 'Vector3D'.ts(7053) */
매개변수 v 는 구조적 타이핑에서 열려있기 때문에 다음과 같은 변수가 삽입될 수도 있다.
{ x: 3, y: 5, z:4, address: "Seoul"}
추가적인 키 값에 어떤 타입이 매핑될지를 알 수 없기에 타입스크립트는 v[axis]
가 어떤 타입으로 될지 모르는 것이다.
타입스크립트의 타입 시스템은 as any로 무력화시킬 수 있기 때문에 아무리 정적 타이핑을 열심히 해놓는다고 하더라도 any를 사용하게 되는 순간 타입스크립트의 수많은 장점을 누리지 못하게 된다.
string 타입은 number 타입이 필요한 곳에서 예상치 못한 런타임 에러를 일으킬 수 있다.
let secretCode: number = 12345;
secretCode = "123456" as any
모듈 개발자가 함수 시그니처에 타입을 적용해놓았다고 하더라도 as any를 사용하는 순간 프로그램의 신뢰도는 떨어질 수 밖에 없다.
function calculateAge(birthDate: Date): number {
//...
return 0;
}
calculateAge("1990-01-42" as any);
타입스크립트의 모토는 확장 가능한 자바스크립트
라고 한다. 타입스크립트의 장점을 최대로 누려야 동료들과의 생산성도 향상될것이다.