스터디/이펙티브 타입스크립트

[이펙티브 타입스크립트] 1장 타입스크립트 알아보기

paran21 2024. 5. 11. 23:45

1장은 타입스크립트의 언어적 특징에 대해 설명하고 있다.

특히 자바, C++과 같은 다른 언어와 어떤 차이가 있는지, 자바스크립트와는 어떤 관계가 있는지에 대한 내용이다.

 

아이템 1. 타입스크립트와 자바스크립트의 관계

타입스크립트는 또 다른 고수준 언어인 자바스크립트로 컴파일되고 실행된다.

그렇기 때문에 자바스크립트와의 관계를 이해하는 것이 매우 중요하다.

## 타입스크립트는 자바스크립트의 상위집합이다.

 

자바스크립트에 타입스크립트를 일부만 적용할 수 있어, 점진적으로 마이그레이션이 가능하다.

그렇지만 타입스크립트는 타입을 명시하는 추가적인 문법을 가지고 있기 때문에 모든 자바스크립트가 타입스크립트의 타입 체커를 통과하는 것은 아니다.

 

## 타입스크립트의 타입 시스템은 자바스크립트의 런타임 동작을 모델링한다.

다른 언어에서는 에러가 발생할 수 있는 상황이어도 자바스크립트에서 정상적으로 동작하는 코드는에러가 발생하지 않는다.

const x = 2 + '3'; //'23'

 

반면에, 자바스크립트에서는 문제가 없어도 의도치 않은 오류로 이어질 수 있는 코드는 오류로 표시한다.

//The value 'null' cannot be used here.(18050)
const a = 7 + null;

//Operator '+' cannot be applied to types 'never[]' and 'number'.(2365)
const b = [] + 12;

//Expected 0-1 arguments, but got 2.(2554)
alert('Hello', 'Typescript')

 

## 타입 체크를 통과하더라도 런타임 오류가 발생할 수 있다.

// 실행하면 에러 발생
// TypeError: Cannot read properties of undefined (reading 'toUpperCase')
const names = ['Alice', 'Bob'];
console.log(names[2].toUpperCase());

 

타입시스템은 정적 타입의 정확성을 보장하지 않는다.

 

 

아이템 2. 타입스크립트의 설정

tsconfig.json 설정 파일을 통해 명시적으로 어떤 설정을 했는지 표현하는게 좋다.

타입스크립트는 어떻게 설정하느냐에 따라 완전히 다른 언어처럼 느껴질 수 있다.

프로젝트가 거대해질수록 설정 변경이 어려워지기 때문에, 가능한 한 초반에 설정하는 것이 좋다.

 

여기서 권장하는 설정은 다음 두 가지이다.

{
  "compilerOptions": {
    // 암묵적인 any를 허용하지 않는다(반드시 타입 명시 필요)
    "noImplicitAny": true
    // 모든 타입에 null과 undefined를 허용하지 않는다.
    // null이나 undefined를 허용할 때는 분명하게 타입으로 명시해야한다.
    "strictNullChecks": true
  }
}


strict 설정을 하면 noImplicitAny와 strictNullChecks 모두 설정된다.

 

 

아이템 3. 코드 생성과 타입이 관계없음을 이해하기

타입스크립트는 자바스크립트로 트랜스파일될 때 모든 인터페이스, 타입, 타입 구문이 제거된다.

타입 오류가 있는 코드도 트랜스파일이 가능하다.

(tsconfig.json의 noEmitOnError 설정을 통해 빌드를 막을 수 있다.)

 

## 런타임에 타임 정보를 유지하는 방법

1. 특정 속성이 존재하는지를 체크한다.

// shape에 'height'이라는 속성이 있는지 확인
if ('height' in shape) { ... }

 

2. 런타입에 접근 가능한 타입 정보를 명시적으로 저장한다(태그된 유니온 tagged union)

// 타입 정보를 저장하기 위해 kind를 추가
interface Square {
    kind: 'square';
    size: number;
}

// kind를 통해 타입을 확인할 수 있다.
function calculateArea(shape: Shape) {
    if (shape.kind === 'square') { ... }
}

 

3. 타입을 클래스로 선언

class Square {}
class Rectangle extends Square {}
type Shape = Square | Rectangle;

function draw(shape: Shape) {
    // class로 선언하지 않았다면 instanceof 사용 불가
    // interface로 선언하면 'Square' only refers to a type, but is being used as a value here.(2693)
    if (shape instanceof Square) { ... }
}

 

인터페이스는 타입으로만 사용되지만 클래스는 타입과 값으로 모두 사용가능하기 때문에 instanceof를 통해 타입을 확인할 수 있다.

 

## 타입 단언은 타입을 변환하지 않는다.

타입 단언은 런타임 동작에 아무런 영향을 주지 않는다.

타입을 변환하기 위해서는 런타임에 타입을 체크하고 자바스크립트 연산을 통해 변환을 수행해야 한다.

function asNumber(val: number | string) {
    // as number은 string을 number로 변환하지 않는다.
    // return val as number;
    return typeof val === 'string' ? Number(val) : val;
}

 

## 런타임 타입은 선언된 타입과 다를 수 있다.

타입 선언은 모두 삭제되기 때문에 API를 잘못 파악하거나, 배포 후 API가 변경된 경우에는 의도와 다른 타입이 들어와 에러가 발생할 수 있다.

선언된 타입은 항상 달라질 수 있다는 점을 주의해야 한다.

 

## 타입스크립트 타입으로는 함수를 오버로딩할 수 없다.

타입이 모두 삭제되기 때문에 함수 오버로딩은 불가능하다.

function add(a: number, b: number) {
  return a + b;
}

// Duplicate function implementation.(2393)
function add(a: string, b: string) {
  return a + b;
}

// 동일한 이름의 함수에 대해 다른 타입으로 선언할 수 있지만 다음의 방식은 권장되지 않는다(아이템 50)
function add(a: number, b: number): number;
function add(a: string, b: string): string;

function add(a: any, b: any) {
  return a + b;
}

 

## 타입스크립트의 타입이 런타임 성능에 영향을 주지 않는다

빌드 타임 오버헤드가 있지만, 타입스크립트 컴파일은 상당히 빠른 편이다.

오래된 런타임 환경을 지원하기 위해 호환성을 높이고 성능 오버헤드를 높일 수 있지만, 타켓과 언어 레벨의 문제로 타입과는 관련이 없다.

 

 

아이템 4. 구조적 타이핑

## 자바스크립트는 덕 타이핑(duck typing)에 기반

자바스크립트는 객체가 어떤 타입에 부합하는 변수와 메서드를 가질 경우 객체를 해당 타입에 속하는 것으로 간주한다.

타입스크립트에서도 선언된 타입이 달라도 구조가 호환된다면 타입 체크를 통과한다.(구조적 타이핑 structural typing)

interface Vector2D {
  x: number;
  y: number;
}

interface NamedVector2D {
  x: number;
  y: number;
  name: string;
}

function calculateLength(v: Vector2D) {
  return Math.sqrt(v.x * v.x + v.y * v.y);
}

const namedVector = {x: 2, y: 3, name: 'vector'};

// namedVector에는 x, y가 있기 때문에 가능
calculateLength(namedVector);

 

즉, 타입스크립트의 타입 시스템은 다른 언어와 달리 열려(open)있고, 타입이 선언된 속성 외에 임의의 속성을 추가해도 오류가 발생하지 않는다.

const vec3D = {x: 3, y: 4, z: 1, address: '123 Broadway'};
calculateLengthL1(vec3D); //정상, NaN을 반환

 

클래스의 경우에도 필요한 속성과 생성자가 존재하면 타입 체크에 통과한다.

class C {
  foo: string;  
  constructor(foo: string) {
  this.foo = foo;  
  }
}

const c = new C('instance of C');

// foo 속성을 갖고 있고,
// 하나의 매개변수로 호출되는 생성자를 가짐(Object.prototype)
// C타입에 할당할 수 있음
const d: C = { foo: 'object literal' };

 

## 구조적 타이핑의 장점

- 테스트를 작성할 때 유리하다.

추상화를 통해 로직과 테스트를 특정한 구현으로부터 분리할 수 있다.

interface DB {
  runQuery: (sql: string) => any[];
}

//database를 PostgresDB가 아니라 추상화한 DB를 통해 테스트를 용이하게 할 수 있다.
function getAuthors(database: DB): Author[] {
  const authorRows = database.runQuery(SELECT FIRST, LAST FROM AUTHORS);
  return authorRows.map(row => ({first: row[0], last: row[1]}));
}

 

- 라이브러리 간 의존성을 완벽히 분리할 수 있다(아이템51)

 

 

아이템 5. any 타입 지양하기

타입스크립트는 타입을 조금씩 추가할 수 있고(점진적), 언제든지 타입 체크를 해제할 수 있는다.(선택적)

이러한 성격을 가장 잘 보여주는 것이 any이다.

any를 사용하면 타입스크립트의 수많은 장점을 누를 수 없기 때문에, 부득이한 경우에도 위험성을 알고 사용해야 한다.

 

## any를 권장하지 않는 이유

- 타입 안정성이 없다 : 어떤 타입도 할당할 수 있다.

let age: any;
//number가 아니라 any로 선언하면 사용하는 곳에서 string을 할당해도 에러가 나지 않는다.
age = '12';

//age는 '121'
age += 1;


- 함수 시그니처를 무시한다.

function calculateAge(birthDate: Date): number {
...
}

//타입을 any로 선언했기 때문에 에러가 나지 않는다.
let birthDate: any = '1990-01-19';
calculateAge(birthDate);  // 정상


- 자동완성 기능과 rename 등 언어서비스가 적용되지 않는다.
- 리펙토링 때 버그를 감춘다 : 오류를 발견하기 어렵다.
- 타입 설계가 불분명해진다.
- 타입시스템의 신뢰도를 떨어트린다.