[Effective Typescript] 1장 타입스크립트 알아보기

@1000peach2022. 05. 05  -  ☕️ 7 min read
[Effective Typescript] 1장 타입스크립트 알아보기

해당 포스트는 댄 밴더캄의 이펙티브 타입스크립트 책 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

  • nullundefined가 모든 타입에서 허용되는 지 확인
  • noImplicitAny: true 설정이 우선해야 한다.
// strictNullChecks: false --> 정상 // strictNullChecks: true --> 에러. undefined도 동일 const x: number = null // strictNullChecks: true일 때는 명시적으로 null을 타입에 추가해야 함. const x: number | null = null
  • false 상태에서 null값을 허용하지 않으려면 체크 코드나 단언문을 작성해야 한다.
  • true로 설정할 경우 nullundefined 오류에 큰 도움이 되지만 처음 타입스크립트 작성이 어려울 수 있다.

타입스크립트의 모든 체크를 설정하고 싶다면 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는 타입 설계를 감추고 (코드 가독성 저하) 타입 시스템의 신뢰도를 떨어뜨려 오류 발생률을 증가시킨다.

🔗 참고

이전 게시글

[React] react-sortable-hoc

다음 게시글

[Jest] Jest 시작하기

profile

@1000peach

    GitHubGmailPortfolio
© 2022 1000peach, Powered By Gatsby.