아이템 1: 타입스크립트와 자바스크립트의 관계 이해하기 - TS vs JS
"타입스크립트는 자바스크립트의 superset(상위집합)이다."
"타입스크립트는 타입이 정의된 자바스크립트의 superset(상위집합)이다."
Superset
타입스크립트는 문법적으로도 자바스크립트의 상위집합이다.
JS에 문법 오류 x => 유효한 타입스크립트 프로그램
JS에 이슈 => 문법 오류가 아니더라도, 타입 체커에게 지적
문법의 유효성과 동작 이슈는 독립적 문제
부분집합
모든 자바스크립트 프로그램은 타입스크립트다 => O
JS -> TS 마이그레이션에 이점
모든 타입스크립트 프로그램은 자바스크립트다 => X
타입을 명시하는 추가적인 문법을 가지기 때문
자바스크립트와 타입 체커
let city = "new york city";
console.log(city.toUppercase());
// ~~~~~~~~~~~ Property 'toUppercase' does not exist on type
// 'string'. Did you mean 'toUpperCase'?
타입 체커는 타입 추론을 통해, 자바스크립트 프로그램에서도 유용하게 오류를 찾아낼 수 있다.
런타임에 오류를 발생시킬 코드 찾기: 정적 타입 시스템
const states = [
{ name: "Alabama", capital: "Montgomery" },
{ name: "Alaska", capital: "Juneau" },
{ name: "Arizona", capital: "Phoenix" },
// ...
];
for (const state of states) {
console.log(state.capitol);
// ~~~~~~~ Property 'capitol' does not exist on type
// '{ name: string; capital: string; }'.
// Did you mean 'capital'?
// 오타를 찾을 수 있지만, 어느 쪽이 오타인지 알 수 없다.
}
// 명시적으로 States 선언 시
interface State {
name: string;
capital: string;
}
const states: State[] = [
// 타입 구문을 추가하면 오류를 찾을 수 있다
{ name: "Alabama", capital: "Montgomery" },
{ name: "Alaska", capital: "Juneau" },
{ name: "Arizona", capitol: "Phoenix" },
// ~~~~~~~~~~~~~~~~~~ Object literal may only specify known
// properties, but 'capitol' does not exist in type
// 'State'. Did you mean to write 'capital'?
// ...
];
for (const state of states) {
console.log(state.capital);
}
타입 체커를 통과한 타입스크립트 프로그램
평소 작성하는 타입스크립트 코드 = 오류가 없는 상태의, 타입 체커를 통과한 프로그램
타입스크립트의 타입 시스템은 자바스크립트의 런타임 동작을 '모델링' 한다.
또한, 의도치 않은 코드가 오류로 이어질 수 있는 경우를 고려함
const x = 2 + "3"; // OK, type is string
const y = "2" + 3; // OK, type is string
자바스크립트에서는 오류가 발생하지 않으나, 타입 체커를 문제점을 표시한다.
const a = null + 7; // Evaluates to 7 in JS
// ~~~~ Operator '+' cannot be applied to types ...
const b = [] + 12; // Evaluates to '12' in JS
// ~~~~~~~ Operator '+' cannot be applied to types ...
alert("Hello", "TypeScript"); // alerts "Hello"
// ~~~~~~~~~~~~ Expected 0-1 arguments, but got 2
타입스크립트가 이해하는 값의 타입과 실제 값의 차이가 있기 때문에 오류가 발생하지 않는 코드
const names = ["Alice", "Bob"];
console.log(names[2].toUpperCase());
타입 시스템은 정적 타입의 정확성을 보장하는 것이 목적이 아님
정확성 보장 목적이라면? Reason, Elm 같은 언어를 선택한다. (런타임 안정성을 보장하는 대신, JS의 Superset이 아니므로 마이그레이션 과정이 복잡하다.)
✔️ 요약
타입스크립트는 자바스크립트의 Superset이다.
모든 자바스크립트 프로그램은 이미 타입스크립트 프로그램이다. 반대로, 타입스크립트는 별도의 문법을 가지고 있기 때문에 일반적으로는 유효한 자바스크립트 프로그램이 아니다.
타입스크립트는 자바스크립트 런타임 동작을 모델링하는 타입 시스템을 가지고 있다.
런타임 오류를 발생시키는 코드를 찾아내려 한다.
다만, 타입 체커를 통과하면서도 런타임 오류를 발생키는 코드는 충분히 존재한다.
타입스크립트 타입 시스템은 전반적으로 자바스크립트 동작을 모델링한다.
그러나, 잘못된 매개변수 개수와 같이 자바스크립트에서는 허용되지만 타입스크립트에서는 문제가 되는 경우가 있으며 엄격함은 온전히 취향의 차이다.
아이템 2: 타입스크립트 설정 이해하기 - Which TS
설정
cli
tsc --noImplicitAny program.ts
tsconfig.json
{"compilerOptions": {"noImplicitAny": true}}
tsconfig.json을 사용하는 게 좋은 이유
타입스크립트를 어떻게 사용할 계획인지, 동료나 다른 도구가 알 수 있다
tsc --init
{"strict": true}
설정을 통해, 대부분의 오류를 잡아낼 수 있다.타입스크립트 코드를 공유했을 때 타입 체커 결과가 다르다면, 동일한 컴파일러 설정을 갖고 있는지 확인해야 한다.
implicitAny
암시적인, 암묵적으로 합의된 any
true로 타입 명시하도록 해야 하는 이유: 타입스크립트가 문제를 발견하기 수월하고, 코드의 가독성이 좋아지며, 개발자의 생산성이 향상
false로 두는 경우: 마이그레이션 등 일부 경우
// ✅ tsConfig: {"noImplicitAny":false}
function add(a, b) {
return a + b;
}
// ❌ tsConfig: {"noImplicitAny":true}
function add(a, b) {
// ~ Parameter 'a' implicitly has an 'any' type
// ~ Parameter 'b' implicitly has an 'any' type
return a + b;
}
// (타입 명시) tsConfig: {"noImplicitAny":true}
function add(a: number, b: number) {
return a + b;
}
strictNullChecks
null과 undefined가 모든 타입에서 허용되는가
true로 null 체크해야 하는 이유: 'undefined는 객체가 아닙니다'와 같은 런타임 오류가 발생할 수 있다.
false로 두는 경우: 마이그레이션 등 일부 경우
strictNullChecks가 true이려면, noImplicitAny가 먼저 true여야 함
// tsConfig: {"noImplicitAny":true,"strictNullChecks":false}
const x: number = null; // OK, null is a valid number\
// tsConfig: {"noImplicitAny":true,"strictNullChecks":true}
const x: number = null;
// ~ Type 'null' is not assignable to type 'number'
const x: number | null = null; // null 허용을 명시적으로 드러냄
null 체크 코드 or 단언
// tsConfig: {"noImplicitAny":true,"strictNullChecks":true}
const el = document.getElementById("status");
el.textContent = "Ready";
// ~~ Object is possibly 'null'
if (el) {
// (1) null 체크하기
el.textContent = "Ready"; // OK, null has been excluded
}
// assertion, 단언
el!.textContent = "Ready"; // OK, we've asserted that el is non-null
✔️ 요약
타입스크립트 컴파일러는 언어의 핵심 요소에 영향을 미치는 설정을 포함한다
cli보다는 tsconfig.json을 사용하는 것이 좋다
엄격한 체크를 위해 strict 설정을 고려한다.
noImplicitAny와 strictNullChecks를 설정하는 것이 좋다.
아이템 3: 코드 생성과 타입이 관계없음을 이해하기 - Independent
타입스크립트 컴파일러
transpile 트랜스파일: 최신 타입스크립트, 자바스크립트를 브라우저에서 동작할 수 있도록 구버전의 자바스크립트로 트랜스파일 한다.
코드의 타입 오류를 체크한다.
Translate과 Compile이 합쳐져 트랜스파일이라는 신조어가 탄생함
소스코드를 동일한 동작을 하는 다른 형태의 소스코드 (다른 버전, 다른 언어)로 변화하는 행위. 결과물이 "컴파일되어야하는 소스코드"이기 때문에 컴파일과 구분하여 부른다.
1과 2는 독립적
1 => 타입스크립트가 자바스크립트로 변환될 때, 코드 내의 타입에는 영향을 주지 않는다. 자바스크립트의 실행 시점에도 타입은 영향을 미치지 않음
즉, 타입 오류가 있는 코드도 컴파일이 가능하다.
Compile 컴파일
컴파일은 타입 체크와 독립적으로 동작하기 때문에, 타입 오류가 있는 코드도 컴파일이 가능
문제가 될 부분을 알려주지만, 빌드를 멈추지는 않는다.
오류가 있더라도 컴파일된 산출물이 있다면, 다른 부분 테스트 가능
오류가 있을 때 컴파일하지 않으려면 tsconfig.json에
{"noEmitOnError":true}
설정
컴파일과 타입 체크
코드에 오류가 있을 때, "컴파일에 문제가 있다"고 말하는 경우 기술적으로 틀린말!
엄밀히 코드 생성만 컴파일이라고 할 수 있음
타입스크립트가 유효한 자바스크립트라면? 컴파일을 한다.
그러므로, 코드에 오류가 있다? "타입 체크에 문제가 있다"고 말하는 것이 정확하다
런타임에는 타입 체크 X
interface Square {
width: number;
}
interface Rectangle extends Square {
height: number;
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
// ❗️ instanceof은 런타임에 일어나지만 Ractangle은 타입이다
// 인터페이스, 타입, 타입 구문은 JS로 컴파일되는 과정에서 제거된다. => erasable
if (shape instanceof Rectangle) {
// ~~~~~~~~~ 'Rectangle' only refers to a type,
// but is being used as a value here
return shape.width * shape.height;
// ~~~~~~ Property 'height' does not exist
// on type 'Shape'
} else {
return shape.width * shape.width;
}
}
런타임에 타입 정보를 유지하는 방법 A: 속성 체크
attribute 속성 체크는 런타임에만 접근 가능한 값에 관련
타입 체커도 shape의 타입을 Rectangle로 보정하기 때문에 오류 사라짐
interface Square {
width: number;
}
interface Rectangle extends Square {
height: number;
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
// 속성 존재 여부를 체크~
if ("height" in shape) {
shape; // Type is Rectangle
return shape.width * shape.height;
} else {
shape; // Type is Square
return shape.width * shape.width;
}
}
런타임에 타입 정보를 유지하는 방법 B: Tagged Union
런타임에 접근 가능한 타입을 정보를 명시적으로 저장하는 태그 기법
interface Square {
// union이니까 ~ tagged union
kind: "square"; // 런타임에 접근 가능한 tag
width: number;
}
interface Rectangle {
kind: "rectangle";
height: number;
width: number;
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
if (shape.kind === "rectangle") {
// tag 체크
shape; // Type is Rectangle
return shape.width * shape.height;
} else {
shape; // Type is Square
return shape.width * shape.width;
}
}
런타임에 타입 정보를 유지하는 방법 C: 타입을 클래스로 만들기
인터페이스는 타입으로만 사용 가능하지만, 클래스로 선언하면 타입과 값으로 모두 사용할 수 있다. (타입과 값으로 참조)
class Square {
constructor(public width: number) {}
}
class Rectangle extends Square {
constructor(public width: number, public height: number) {
super(width);
}
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
// 타입으로 참조
if (shape instanceof Rectangle) {
// 값으로 참조
shape; // Type is Rectangle
return shape.width * shape.height;
} else {
shape; // Type is Square
return shape.width * shape.width; // OK
}
}
타입 연산은 런타임에 영향을 주지 않는다.
// ts 코드
function asNumber(val: number | string): number {
return val as number; // 타입 단언
}
// 변환된 js
function asNumber(val) {
return val; // 타입 단언은 타입 연산이고, 런타임 동작에 아무런 영향을 미치지 않으므로, 값을 정제하기 위해서는 런타임의 타입을 체크하고, 자바스크립트 연산을 통해 변환을 수행해야 한다.
}
런타임 타입은 선언된 타입과 다를 수 있다.
타입스크립트는 Dead Code 죽은 코드를 찾아낼 수 있다.
예외: api response
타입스크립트 타입으로는 함수를 오버로드 할 수 없다.
C++ 같은 언어에서는 함수 오버로딩이 가능함
타입스크립트는 오버로딩 안됨
function add(a: number, b: number) {
// ~~~ Duplicate function implementation
return a + b;
}
function add(a: string, b: string) {
// ~~~ Duplicate function implementation
return a + b;
}
{"noImplicitAny":false}
, 타입스크립트는 함수 오버로딩 기능이 있지만 타입 수준에서만 동작선언문이 여러개여도 implementation (구현체)는 오직 하나
// tsConfig: {"noImplicitAny":false}
function add(a: number, b: number): number; // 얘가 진짜
function add(a: string, b: string): string;
function add(a, b) {
return a + b;
}
const three = add(1, 2); // Type is number
const twelve = add("1", "2"); // Type is string
타입스크립트 타입은 런타임 성능에 영향을 주지 않는다.
타입과 타입 연산자는 JS 변환 시점에 제거되기 때문에, 런타임 성능에 영향을 주지 않는다.
타입스크립트의 정적 타입은 비용이 전혀 들지 않는다.
특징
'런타임' 오버헤드가 없는 대신, 타입스크립트 컴파일러는 '빌드타임' 오버헤드가 있다.
컴파일 속도가 빠름: 증분(incremental) 빌드시 체감
오버헤드가 커지면, 빌드 도구에서 transpile only 설정(타입 체크를 건너뜀)
호환성과 성능 사이의 선택
호환성: 오래된 런타임 환경을 지원하기 위해 성능 오버헤드 감안
ex) ES5 타깃으로 컴파일 시, 특정 헬퍼 코드 추가
성능: 성능 중심의 네이티브 구현체 선택
✔️ 요약
코드 생성은 타입 시스템과 무관하다.
런타임 동작이나 성능에 영향을 주지 않는다.
타입 오류가 존재하더라도 코드 생성(컴파일) 가능 (noEmitOnError로 막기 가능)
타입은 런타임에 사용할 수 없다.
런타임에 타입을 지정하려면, 타입 정보 유지를 위한 별도의 방법
ex) 태그된 유니온, 속성 체크, 클래스
아이템 4: 구조적 타이핑 익숙해지기 - Stuctural
자바스크립트는 duck typing
함수의 매개변수 값이 모두 제대로 주어진다면, 값이 어떻게 만들어졌는지 신경쓰지 않고 사용
타입스크립트도 동작, 매개변수 값이 요구사항을 만족한다면 타입이 무엇인지 신경쓰지 않는 동작을 그대로 모델링한다.
다만, 타입 체커의 타입에 대한 이해도가 사람과 다름
구조적 타이핑을 제대로 이해한다면 오류와 오류가 아닌 경우의 차이를 알고 견고한 코드 작성 가능
interface Vector2D {
x: number;
y: number;
}
function calculateLength(v: Vector2D) {
return Math.sqrt(v.x * v.x + v.y * v.y);
}
interface NamedVector {
name: string;
x: number;
y: number;
}
const v: NamedVector = { x: 3, y: 4, name: "Zee" };
calculateLength(v); // OK, result is 5
// Vector2D, NamedVector 사이의 관계를 선언하지 않았다
// NamedVector가 Vector2D와 호환되는 구조이기 때문에, calculateLength 호출이 가능하다 => 구조적 타이핑
구조적 타이핑으로 발생한 문제 경우
interface Vector2D {
x: number;
y: number;
}
function calculateLength(v: Vector2D) {
return Math.sqrt(v.x * v.x + v.y * v.y);
}
interface NamedVector {
name: string;
x: number;
y: number;
}
interface Vector3D {
x: number;
y: number;
z: number;
}
function normalize(v: Vector3D) {
const length = calculateLength(v); // 2D벡터를 받도록 선언된 함수에서 3D벡터를 받았음에도 타입 체커가 문제로 인식하지 않음
return {
x: v.x / length,
y: v.y / length,
z: v.z / length,
};
} // 결국 1로 normalize되지 않았다.
타입스크립트의 타입 시스템은 열려 있다(open)
타입이 확장에 열려있다. 타입에 선언된 속성 외에 임의의 속성을 추가하더라도 오류가 발생하지 않는다.
function calculateLengthL1(v: Vector3D) {
let length = 0;
for (const axis of Object.keys(v)) {
const coord = v[axis];
// ~~~~~~~ Element implicitly has an 'any' type because ...
// 'string' can't be used to index type 'Vector3D'
length += Math.abs(coord);
}
return length;
}
const vec3D = { x: 3, y: 4, z: 1, address: "123 Broadway" };
// 숫자 아닌 값을 넣어도, 호환되는 구조로 함수 실행 (구조적 타이핑)
calculateLengthL1(vec3D); // OK, returns NaN
v는 어떤 속성이든지 가질 수 있기 때문에, v[axis]가 number라고 확정할 수 없다.
정확한 타입으로 객체를 순회하기 까다로움
function calculateLengthL1(v: Vector3D) {
return Math.abs(v.x) + Math.abs(v.y) + Math.abs(v.z); // 루프보다 이게 나음 (아이템 54에서 다룰 것)
}
클래스의 경우
class C {
foo: string;
constructor(foo: string) {
this.foo = foo;
}
}
const c = new C("instance of C");
const d: C = { foo: "object literal" }; // OK!
// 구조적으로, 필요한 속성과 생성자가 존재하기 때문에 문제가 없다
// 단순 할당이 아닌 연산 로직이 존재한다면? d의 경우 생성자를 실행하지 않으므로 문제가 발생하게 된다.
// 1️⃣ 메서드가 있을 경우, 구조적으로 다름
const d: C = { foo: "object literal" };
// ~ Property 'hi' is missing in type '{ foo: string; }' but required in type 'C'.
// 2️⃣ 단순 할당이 아니라 문제가 생기는 경우
class C {
foo: string;
constructor(foo: string) {
this.foo = foo + " with bar";
}
}
const c = new C("instance of C");
const d: C = { foo: "object literal" };
console.log(c.foo); // "instance of C with bar"
console.log(d.foo); // "object literal"
테스트를 작성할 때
DB에 쿼리하고 결과를 처리하는 함수
interface PostgresDB {
runQuery: (sql: string) => any[];
}
interface Author {
first: string;
last: string;
}
interface DB {
runQuery: (sql: string) => any[];
}
function getAuthors(database: DB): Author[] {
const authorRows = database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`);
return authorRows.map((row) => ({ first: row[0], last: row[1] }));
}
// test 시 용이
// 추상화(DB)해서, 로직과 테스트를 특정한 구현(PostgresDB)으로부터 분리함
// 라이브러리 간의 의존성을 완벽히 분리할 수 있다
test("getAuthors", () => {
const authors = getAuthors({
runQuery(sql: string) {
return [
["Toni", "Morrison"],
["Maya", "Angelou"],
];
},
});
expect(authors).toEqual([
{ first: "Toni", last: "Morrison" },
{ first: "Maya", last: "Angelou" },
]);
});
✔️ 요약
자바스크립트는 덕 타이핑 기반
타입스크립트가 이를 모델링하기 위해 구조적 타이핑을 사용함을 이해해야 한다.
어떤 인터페이스에 할당 가능한 값이라면, 타입 선언에 명시적으로 나열된 속성들을 가지고 있을 것. (구조적으로 호환)
타입은 봉인되어있지 않다
클래스도 구조적 타이핑 규칙을 따른다. 인스턴스가 예상과 다를 수 있다.
구조적 타이핑을 사용하면 유닛 테스팅이 용이하다.
아이템 5: any 타입 지양하기 - Any
타입스크립트의 타입 시스템은 점진적(gradual)이고 선택적(optional)
점진적: 코드에 타입을 조금씩 추가할 수 있다.
선택적: 언제든지 타입 체커를 해제할 수 있다.
let age: number;
age = "12";
// ~~~ Type '"12"' is not assignable to type 'number'
age = "12" as any; // OK
any 타입에는 타입 안정성이 없다.
혼돈!
let age: number;
age = "12" as any;
age += 1; // OK; at runtime, age is now "121"
any는 함수 contract (시그니처)를 무시한다.
함수
호출하는 쪽은 약속된 타입의 입력을 제공
함수는 약속된 타입의 출력 반환
function calculateAge(birthDate: Date): number {
// birthDate는 Date 아님!
...
}
let birthDate: any = "1990-01-19";
calculateAge(birthDate); // OK
자바스크립트는 종종 암시적으로 타입이 변환되기 때문에, string 타입이 number 타입이 필요한 곳에서 오류 없이 실행될 때가 있고, 그럴 경우 문제 발생
any 타입에는 언어 서비스가 적용되지 않는다.
타입스크립트 언어 서비스
심벌에 타입이 있다면, 자동완성과 적절한 도움말이 제공됨
Rename Symbol 안됨
타입스크립트의 모토 "확장 가능한 자바스크립트": 생산성 향상!
any 타입은 코드 리팩터링 때 버그를 감춘다.
타입 체커를 통과함에도, 런타임에 오류가 발생한다.
any가 아닌 구체적인 타입을 사용했다면, 오류를 발견했을 것
interface ComponentProps {
onSelectItem: (item: any) => void;
}
function renderSelector(props: ComponentProps) {
/* ... */
}
let selectedId: number = 0;
function handleSelectItem(item: any) {
selectedId = item.id;
}
renderSelector({ onSelectItem: handleSelectItem });
// ComponentProps를 아래 코드로 바꿨을 때, handleSelectItem의 오류를 런타임까지 알아볼 수 없다. 타입 체커를 통과하기 때문.
interface ComponentProps {
onSelectItem: (id: number) => void;
}
any는 타입 설계를 감춘다
객체를 정의할 때 특히 문제! 쓰지마세요
상태 객체의 설계를 감춰버리기 때문
깔끔하고 정확하고 명료한 코드 작성을 위해 타입 설계는 필수다
동료가 코드 검토 시, 애플리케이션의 상태를 어떻게 변경했는지 코드부터 재구성해야 함
설계가 명확히 보이도록 타입을 작성하자
any는 타입 시스템의 신뢰도를 떨어뜨린다
타입 체커가 휴먼 에러를 잡아주고, 코드의 신뢰도를 높임
런타임에 타입 오류가 생긴다면? 타입 체커를 신뢰할 수 없을 것
any 타입이 많다면? 골치 아픔
타입 오류 수정
실제 타입 기억
✔️ 요약
any 타입을 사용하면, 타입 체커와 타입스크립트 언어를 무력화
진짜 문제점을 감추고, 개발 경험을 악화시키고, 타입 시스템의 신뢰도를 떨어뜨린다.
쓰지 마세요~