해당 포스트는 댄 밴더캄의 이펙티브 타입스크립트 책 1장을 읽고 정리한 내용입니다.
타입스크립트가 무엇인 지, 자바스크립트와의 관계 등 타입스크립트의 기본적인 개념을 다루고 있습니다.
1. 아이템1 - 타입스크립트 알아보기
타입스크립트는 자바스크립트의 상위 집합이다.
- 타입스크립트는 컴파일과 실행 모두 자바스크립트로 이루어진다.
- 상위 집합이다 === 마이그레이션이 쉽다. 기존 코드를 유지하며 일부분에만 타입 적용이 가능하기 때문
- 타입스크립트는
정적 타입 언어
로컴파일 시점
에서 타입 오류를 발견하고, 자바스크립트는동적 타입 언어
로런타임
시 타입 오류가 발생한다.
타입스크립트 타입 시스템의 목표
- 런타임에 오류를 발생시킬 부분을 미리 찾는 것 -> 타입 시스템의 기본 원칙은 자바스크립트의 런타임 동작을 모델링하기 때문이다.
- 타입스크립트 컴파일러는 오류는 아니지만 의도와 다르게 동작하는 코드도 찾아낸다.
명시적으로 타입이 있어도 타입 구문을 추가하면 코드의 의도
가 무엇인 지 알려주기 때문에 더 많은 오류를 찾을 수 있다.
interface State {
name: string
capital: string
}
const states: State[] = [
{ name: 'Alabama', capital: 'Montgomery' },
{ name: 'Alaska', capitol: 'Juneau' },
// ~~~~~~~~~~~~~~~~~ Did you mean to write 'capital'?
// capital, capitol 변수명이 다른 건 알지만 오타로 인식하지는 못 함.
// 명시적으로 State[] 타입 구문을 추가하여 오타가 발생하는 잠재적인 문제를 해결함.
{ name: 'Arizona', capital: 'Phoenix' },
// ...
]
타입 체크를 통과해도 런타임 오류가 발생할 수 있는데, 근본적인 원인은 타입스크립트가 이해하는 값과 실제 값의 차이
가 있기 때문이다.
const names = ['Alice', 'Bob']
console.log(names[2].toUpperCase())
// TypeError: Cannot read property 'toUpperCase' on undefined
// 배열이 범위 내에서 사용될 것이라 가정이 틀려서 오류가 발생함.
즉 정적 타입의 정확한 보정으로 타입스크립트를 쓰는 것은 아니며 정확성을 요구한다면 마이그레이션이 어려워질 것이다.
2. 아이템2 - 타입스크립트 설정 이해하기
타입 체커
- 타입 체커가 오류를 감지할 지 여부는 설정에 따라 다르다.
- 커맨드 라인 또는 tsconfig.json 설정 파일(권장)에서 확인할 수 있다.
tsc --noImplicitAny programts
{
"compilerOptions": {
"noImplicitAny": true
}
}
noImplicitAny
- 변수들이 미리 정의된 타입을 가져야 하는 지 여부
- 변수에 any 타입이라도
명시해주면
타입 체커를 통과한다. - 대부분
true
로 설정하여 타입을 명시해야 개발에 수월하다 ->false
는 타입스크립트로 마이그레이션하는 상황에서 필요할 것
strictNullChecks
null
과undefined
가 모든 타입에서 허용되는 지 확인noImplicitAny: true
설정이 우선해야 한다.
// strictNullChecks: false --> 정상
// strictNullChecks: true --> 에러. undefined도 동일
const x: number = null
// strictNullChecks: true일 때는 명시적으로 null을 타입에 추가해야 함.
const x: number | null = null
false
상태에서null
값을 허용하지 않으려면 체크 코드나 단언문을 작성해야 한다.true
로 설정할 경우null
과undefined
오류에 큰 도움이 되지만 처음 타입스크립트 작성이 어려울 수 있다.
타입스크립트의 모든 체크를 설정하고 싶다면 strict 설정을 하면 된다. === 엄격한 체크
3. 아이템3 - 코드 생성과 타입이 관계없음을 이해하기
타입스크립트 컴파일러 역할
- 최신 타입스크립트/자바스크립트가 브라우저에서 동작하도록 구버전의 자바스크립트로 트랜스파일 함
- 코드의 타입 오류 체크
타입 오류가 있는 코드도 컴파일이 가능하다.
- 타입 오류와 독립적으로 동작하기 때문이다.
- 타입 오류는 빌드를 멈추지 않는다.
경고
와 비슷함 - 장점: 오류가 있어도 앱이 실행되기 때문에 다른 부분을 테스트할 수 있음
- 오류 발생 시 컴파일하지 않으려면
noEmitOnError
설정이 필요하다.
런타임과 타입 체크
- 타입스크립트의 타입은 제거되므로 런타임에 타입 체크가 불가능하다. ex)
instanceof
- 타입 참조 (런타임 접근 불가) / 값 참조 (런타임 접근 가능)
type Shapre = Square | Rectangle
: Rectangle은 타입으로 참조된다.shape instanceof Rectangle
: 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
}
}
// 태그 기법: 런타임메 접근 가능한 타입 정보를 명시적으로 저장
interface Square {
kind: 'square' // square type 입니다.
width: number
}
interface Rectangle {
kind: 'rectangle' // rectangle type 입니다.
height: number
width: number
}
type Shape = Square | Rectangle
function calculateArea(shape: Shape) {
if (shape.kind === 'rectangle') {
shape // Type is Rectangle
return shape.width * shape.height
} else {
shape // Type is Square
return shape.width * shape.width
}
}
타입 연산과 런타임
- 타입은 컴파일에서 제거된다 === 타입 연산은 런타임에 영향을 주지 않는다 === 코드 정제 과정이 없다.
타입 단언문을 이용해 타입 체커를 통과하도록 처리해도, 자바스크립트로 변환되면 정제 과정이 없는 것을 알 수 있다.
as number
는 타입 연산이기 때문에 제거되며 val
의 타입을 정확히 걸러내지 못한다.
function asNumber(val: number | string): number {
return val as number
}
값을 정제하려면 런타임에 타입 체크를 하고, 연산을 통해 변환해야 한다.
function asNumber(val: number | string): number {
return typeof val === 'string' ? Number(val) : val
}
- 런타임 타입은 선언된 타입와 다를 수 있다. ex) api 호출 응답 값
- 타입스크립트 타입으로는 함수 오버라이드를 할 수 없다.
- 함수 오버로딩: 타입스크립트에서는 타입과 런타임 동작이 무관하여 불가능
- 선언문 작성은 가능하나
구현체는 하나
뿐임 -> 타입 선언이 런타임 시점에서 제거되므로
// 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
4. 아이템4 - 구조적 타이핑에 익숙해지기
타입스크립트는 구조적 타이핑을 이용한다.
- 자바스크립트는 덕 타이핑 기반이다.
덕 타이핑(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
}
interface Vector3D {
x: number
y: number
z: number
}
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'
// 타입이 열려있으므로, 만족만 한다면 매개변수 v = {x: 3, y: 4, z: 1, address: '123 Broadway'}; 형식으로 들어와도 문제가 발생하지 않기 때문임.
// 루프보다 모든 속성을 더하는 경우가 나음.
length += Math.abs(coord)
}
return length
}
클래스도 구조적 타이핑 규칙을 따르므로 인스턴스가 예상과 다를 수 있다.
구조적 타이핑의 장점
- 테스트 작성 시 유리하다 -> DB를 추상화하고 구체적인 인터페이스를 정의할 수 있음
- 라이브러리 간 의존성을 분리할 수 있다.
5. 아이템5 - any 타입 지양하기
any 타입은 많은 위험성이 있으므로 최대한 지양해야 한다.
- any는 함수 시그니처를 무시한다 -> 타입 오류 없이 실행되는 경우가 있고, 다른 쪽에서 문제가 발생할 수 있음
함수 시그니처
: 호출하는 쪽은 약속된 타입의 입력을, 반환하는 쪽은 약속된 타입의 출력을 반환하는 것
- any 타입은 언어 서비스가 적용되지 않는다 -> 개발 생산성, 편의성을 저하시킴
- ex) 자동 완성/도움말, 이름 포매팅 등
- any는 타입 설계를 감추고 (코드 가독성 저하) 타입 시스템의 신뢰도를 떨어뜨려 오류 발생률을 증가시킨다.