이펙티브 타입스크립트 1장

정태호·2023년 7월 26일
0

타입스크립트

목록 보기
5/13
post-thumbnail

타입스크립트 알아보기

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

타입스크립트는 자바스크립트의 상위 집합이다(superset)

→ 자바스크립트 프로그램에 문법 오류가 없다면, 유효한 타입스크립트 프로그램이다.
→ 자바스크립트 프로그램에 이슈가 존재한다면 문법 오류가 아니더라도 타입 체커에서 지적당할 가능성이 높다.
→ 문법의 유효성과 동작의 이슈는 독립적인 문제

모든 자바스크립트는 타입스크립트이지만, 모든 타입스크립트가 자바스크립트는 아니다! .js 파일에 있는 코드는 이미 타입스크립트라고 할 수 있기 때문에 JS -> TS로 마이그레이션하는 데 엄청난 이점이 된다.

타입 시스템의 목표 중 하나는 런타임에 오류를 발생시킬 코드를 미리 찾아내는 것이다.

→ 그러나 타입 체커가 모든 오류를 찾아내지는 않음
→ 타입 체커를 통과하면서 런타임 오류를 발생시키는 코드는 충분히 존재.

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

const x = 2 + '3' // 정상 string 타입
const y = 2 + '3' // 정상 string 타입

위 예제가 다른 언어였다면 런타임 오류가 될 만한 코드지만 타입 체커는 정상으로 인식한다.

타입 체커를 통과 하더라도 런타임 에러가 발생한다.

const name = ['Alice', 'Bob'];
console.log(name[2].toUpperCase()); // TypeError : Cannot read property 'toUpperCase' of undefined

타입스크립트는 앞의 배열이 범위 내에서 사용될 것이라고 가정했지만 실제로는 범위를 벗어나서 런타임 오류가 발생했다.

타입스크립트가 이해하는 값의 타입과 실제 값에 차이가 존재하기 때문에 발생하는 오류들이다. 타입 시스템이 정적 타입의 정확성을 보장해줄 것 같지만 그렇지 않기 때문이다.

아이템 2. 타입스크립트 설정 이해하기

  • 타입스크립트 컴파일러는 언어의 핵심 요소에 영향을 미치는 몇 가지 설정을 포함하고 있다.
  • 설정은 커맨드 라인을 이용하기보다는 tsconfig.json을 사용하자!
  • 엄격한 체크를 하고 싶다면 strict: true 설정을 고려하자.

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

타입스크립트 컴파일러의 역할

  1. 최신 TS/JS를 브라우저에서 동작할 수 있도록 구버전의 자바스크립트로 트랜스파일(transpile) 한다.
  2. 코드의 타입 오류를 체크한다.

이 두가지는 서로 완벽히 독립적이다.

  • 타입스크립트가 자바스크립트로 변환될 때 코드 내의 타입에는 영향을 주지 못한다.
  • 자바스크립트의 실행 시점에서 타입은 영향을 미치지 않는다.

타입 오류가 있는 코드도 컴파일 가능

  • 컴파일은 타입 체크와 독립적으로 동작하기 때문에, 타입 오류가 있는 코드도 컴파일이 가능하다.

오류가 있을 때 컴파일 X -> tsconfig.jsonnoEmitOnError를 설정

런타임에는 타입 체크가 불가능하다.

  • 타입스크립트의 타입은 '제거 가능(erasable)'하다.
  • 자바스크립로 컴파일되는 과정에서 모든 인터페이스,타입,타입 구문은 제거되어 버린다.

    타입은 런타임 이전, 값은 런타임 때

interface Square {
  width: number;
}
interface Rectangle extends Square {
  height: number; 
}
type Shape = Square | Rectangle;

function calculateArea(shape: Shape){
  // Rectangle은 형식만 참조하지만, 여기서 값으로 사용되고 있습니다.
	if(shape instanceof Rectangle) {
      // Shape 형식에 height가 없습니다.
      return shape.width * shape.height; 
    } else {
       return shape.width * shape.width;
    }
}

위 코드에서 shape의 타입을 명확하게 하려면 런타임에 타입 정보를 유지하는 방법이 필요하다. 다음 3가지 방법으로 런타임때 타입 정보를 유지할 수 있다.

1. 속성이 존재하는지 체크

  • 속성 체크는 런타임에 접근 가능한 값에만 관련되지만, 타입 체커도 타입을 보정해 주기 때문에 오류가 사라진다.
function calculateArea(shape: Shape){
	if('height' in shape) { // 타입이 Rectangle
      return shape.width * shape.height; 
    } else { // 타입이 Square
       return shape.width * shape.width;
    }
}

2. 런타임에 접근 가능한 타입 정보를 명시적으로 저장하는 태그기법(tagged union)

interface Square {
  kind: 'square'; // tagged union
  width: number;
}
interface Rectangle {
  kind: 'rectangle';
  width: number;
  height: number; 
}
type Shape = Square | Rectangle;

function calculateArea(shape: Shape){
	if(shape.kind === 'rectangle') { // 타입이 Rectangle
      return shape.width * shape.height; 
    }
  // ...
}

런타임에 타입 정보를 손쉽게 유지할 수 있어 흔하게 볼 수 있다.

3. class로 만들기

  • 타입(런타임 접근 불가)과 값(런타임 접근 가능)을 둘 다 사용하는 기법

인터페이스는 타입으로만 사용 가능, 클래스로 선언하면 타입과 값으로 모두 사용할 수 있으므로 오류가 없다.

타입 연산은 런타임에 영향을 주지 않는다

function adNumber(val: number | string): number {
 return val as number; // number 타입으로 단언한다. 
}
  • 변환된 js
function asNumber(val){
  return val;
}

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

타입스크립트에서 런타임 타입과 선언된 타입이 맞지 않을 수 있기 때문에, 언제든 선언된 타입이 달라질 수 있다는 것을 명심해야한다.

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

타입스크립트에서 타입과 런타임의 동작은 무관하기 때문에 함수 오버로딩 기능을 지원하긴 하지만 온전히 타입 수준에서만 동작한다. 하나의 함수에 여러 개의 선언문은 가능, 구현체는 오직 하나뿐이다.

📌 핵심

  • 타입스트립트 타입은 런타임 동작이나 성능에 영향을 주지 않는다!
  • 타입 오류가 존재해도 코드 생성(컴파일)은 가능하다!

아이템 4. 구조적 타이핑에 익숙해지기

자바스크립트는 본질적으로 덕 타이핑(duck typing) 기반이다. 만약 어떤 함수의 매개변수 값이 모두 제대로 주어진다면, 그 값이 어떻게 만들어 졌는지 신경쓰지 않고 사용한다.

덕 타이핑이란, 객체가 어떤 타입에 부합하는 변수와 메서드를 가질 경우 객체를 해당 타입에 속하는 것으로 간주하는 방식이다.

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

// 벡터의 길이를 구하는 함수 (2D)
function calculateLength(v: Vector2D) {
  return Math.sqrt(v.x * v.x + v.y * v.y);
}

interface Vector3D {
  x: number;
  y: number;
  z: number;
}

// 3D 벡터의 길이를 1로 만드는 정규화 함수
function normalize(v: Vector3D) {
  const length = calculateLength(v);
  return {
    x: v.x / length,
    y: v.y / length,
    z: v.z / length,
  };
}

// 함수 normalize는 1보다 조금 더 긴 길이를 가진 결과를 출력
normalize({x: 3, y: 4, z: 5})
// {x: 0.6, y: 0.8, z: 1}

타입스크립트는 오류를 잡지 못했다. 그 이유는 calculateLength는 2D 벡터를 기반으로 연산하는데, 버그로 인해 normalize가 3D 벡터로 연산되었기 때문이다. z가 정규화 과정에서 무시되었다. 타입 체커는 이 문제를 잡아내지 못했다.

이 원인을 조금 더 살펴보면, Vector3D와 호환되는 {x, y, z} 객체로 calculateLength를 호출하면, 구조적 타이핑 관점에서 x, y가 있어서 Vector2D와 호환된다. 따라서 오류가 발생하지 않았고, 타입 체커가 문제로 인식하지 않았다.

함수를 작성할 때, 호출에 사용되는 매개변수의 속성들이 타입에 선언된 속성만을 가질거라 생각하기 쉽지만(봉인된(sealed), 정확한(pricise) 타입) 타입스크립트의 타입 시스템은 항상 열려(open)있다.

구조적 타이핑이란?

  • 구조적 타이핑은 타입의 구조가 일치하는지 여부를 기준으로 타입을 비교하고 호환성을 결정하는 방식
// 구조적 타이핑을 가정하는 가상의 언어 예제
type Person = {
  name: string;
  age: number;
};
const person1: Person = { name: "Alice", age: 30 };
const person2: Person = { name: "Bob", age: 25 };
// person1과 person2는 같은 타입으로 간주

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

일부 특별한 경우를 제외하고 any를 사용하게 되면 타입스크립트의 수많은 장점을 누릴 수 없게 된다.

1. any 타입에는 타입 안전성이 없다.

age += 1; //"121"
  • number 타입 변수를 as any 를 사용하면 string 타입을 할당할 수 있게 된다.

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

  • 함수를 작성할 때 시그니처를 명시하는데 호출하는 쪽은 약속된 타입의 입력 제공, 함수는 약속된 타입의 출력을 반환해야 한다. 그러나 any를 사용하면 이러한 약속을 어길 수 있다.

3. 언어 서비스가 제공되지 않는다.

  • 자동완성 기능과 적절한 도움말이 제공되지 ㅁ않는다.

4. 코드 리팩토링 때 버그를 감춘다.

  • 타입 체커를 통과함에도 런타임에 오류가 발생할 수 있다.

5. 타입 설계를 감춘다.

  • any 타입을 사용하면 타입 설계가 불분명해져서 설계가 잘 되었는지, 어떻게 되었는지 전혀 알 수가 없다. 설계가 명확히 보이도톡 타입을 일일이 작성하는게 좋다.

6. 타입 시스템의 신뢰도를 떨어뜨린다.

  • any 타입을 쓰지 않으면 런타임에 발견될 오류를 미리 잡을 수 있고 신뢰도를 높일 수 있다.

any 타입을 사용하면 타입 체커타입스크립트 언어 서비스를 무력화시켜 버린다. 진짜 문제점을 감추며 타입 시스템의 신뢰도를 떨어뜨리기 때문에 최대한 사용을 피하도록 하자!!

profile
주니어 프론트엔드 개발자가 되고 싶습니다!

0개의 댓글