iamkanguk.dev

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

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

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

iamkanguk 2024. 5. 12. 02:05

1장에서는 타입스크립트라는 언어에 대해서 설명을 하는 파트이다.
자바스크립트와 타입스크립트의 관계에 대해서 중요하게 다루는 것 같다.

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

타입스크립트는 인터프리터로 실행되는 것도 아니고 C와 Java 같은 저수준 언어로 컴파일 되는 것이 아니다.
또 다른 고수준 언어인 JavaScript로 컴파일 되고 실행 역시 TypeScript가 아닌 JavaScript로 이루어진다.

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

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

타입 체크를 통과해도 런타임에 오류가 발생하는 경우

const names = ['Alice', 'Bob'];
console.log(names[2].toUpperCase());
  • 위의 코드는 타입 체크를 통과했지만 실제로 실행시켜보면(런타임 환경에서) 에러가 발생한다. 왜 타입 체크에서 에러가 발생하지 않을까?
  • names 배열은 문자열의 배열로 추론이 되고 특정 인덱스에 접근하려고 할 때 그 인덱스에 값이 존재한다고 가정한다. 위의 코드에서 names[2]는 string | undefined가 되어야 하지만 실제로는 string으로 타입이 추론이 된다. 그렇기 때문에 에러가 발생하지 않는다.
  • 즉, 타입스크립트가 이해하는 값의 타입과 실제 값에 차이가 있기 때문이다.
const names = ['Alice', 'Bob'];
const index_two = names[2]; // string
console.log(names[2].toUpperCase());

요약정리🤔

- 타입스크립트는 자바스크립트의 상위집합이다. 모든 자바스크립트 프로그램은 이미 타입스크립트 프로그램이다. 반대로, 타입스크립트는 별도의 문법을 가지고 있기 때문에 일반적으로 유효한 자바스크립트 프로그램은 아니다.
- 타입스크립트는 자바스크립트 런타임 동작을 모델링하는 타입 시스템을 가지고 있기 때문에 런타임 오류를 발생시키는 코드를 찾아내려고 한다. 그러나 모든 오류를 찾아내리라 기대하면 안 된다. 타입 체커를 통과하면서도 런타임 오류를 발생시키는 코드는 충분히 존재할 수 있다.
- 타입스크립트 타입 시스템은 전반적으로 자바스크립트 동작을 모델링한다. 그러나 잘못된 매개변수 개수로 함수를 호출하는 경우처럼, 자바스크립트에서는 허용되지만 타입스크립트에서는 문제가 되는 경우도 있다. 이러한 문법의 엄격함은 온전히 취향의 차이이며 우열을 가릴 수 없는 문제이다.

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

타입스크립트 컴파일러는 언어의 핵심 요소에 영향을 미치는 몇 가지 설정을 포함하고 있다.
그 설정을 우리는 커맨드 라인을 이용하기 보다는 tsconfig.json을 통해 설정한다!
그리고 프로젝트가 거대해질수록 설정 변경은 어려워지기 때문에 가급적으면 초반에 설정하는 것이 좋다!

이펙티브 타입스크립트에서는 대표적으로 2개의 옵션 설정을 추천한다.

  • noImplictAny: true // 암묵적인 any를 허용하지 않음. 타입 명시가 필요함
  • strictNullChecks: true // 모든 타입에 null과 undefined를 허용하지 않음

마지막으로 strict 설정을 하면 위의 설정들을 자동으로 설정할 수 있다. 가급적이면 strict 모드를 활성화하고 프로젝트를 시작하는 것이 좋다!

요약정리🤔

- 타입스크립트 컴파일러는 언어의 핵심 요소에 영향을 미치는 몇 가지 설정을 포함하고 있다.
- 타입스크립트 설정은 커맨드 라인을 이용하는 것 보단 tsconfig.json을 사용하는 것이 좋다.
- 자바스크립트 프로젝트를 타입스크립트로 전환하는 것이 아니라면 noImplictAny를 true로 설정하는 것이 좋다.
- "undefined는 객체가 아닙니다" 같은 런타임 오류를 방지하기 위해서는 strictNullChecks를 true로 설정하는 것이 좋다.
- 타입스크립트에서 엄격한 체크를 하고 싶다면 strict 설정을 고려해야 한다.

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

타입스크립트 컴파일러

  • 타입스크립트 컴파일러는 크게 2가지 역할을 수행한다.
    • 최신 타입스크립트/자바스크립트를 브라우저에서 동작할 수 있도록 구버전의 자바스크립트로 트랜스파일(transpile) 한다.
      • transpile = translate(번역) + compile(컴파일)
      • 소스코드를 동일한 동작을 하는 다른 형태의 소스코드로 변환하는 행위를 의미한다. 결과물이 여전히 컴파일되어야 하는 소스코드이기 때문에 컴파일과는 구분한다.
    • 코드의 타입 오류를 체크한다.
  • 이 2가지 역할을 서로 독립적이다. 타입스크립트가 자바스크립트로 변환될 때 코드 내의 타입에는 영향을 주지 않는다. 또한 그 자바스크립트의 실행 시점에도 타입은 영향을 미치지 않는다.

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

// cat test.ts
let x = 'hello';
x = 1234;

// tsc test.ts
// test.ts:2:1 - error TS2322: '1234' 형식은 'string' 형식에 할당할 수 없습니다.

// cat test.js
var x = 'hello';
x = 1234;
  • 컴파일은 타입 체크와 독립적으로 동작하기 때문에 타입 오류가 있는 코드도 컴파일이 가능하다.
  • 참고) C와 Java 같은 언어는 타입 체크와 컴파일이 동시에 이루어진다. 따라서 타입스크립트 오류는 C나 Java 같은 언어들의 경고와 비슷하다 + 타입스크립트는 문제가 되는 부분을 알려주지만 빌드를 멈추지는 않는다.
  • 만약 오류가 있을 때 컴파일하지 않으려면 tsconfig.json에 noEmitOnError를 설정하자.

🔍 컴파일과 타입 체크 🔍

  • 코드에 오류가 있을 때 "컴파일에 문제가 있다" 가 아니라 "타입 체크에 문제가 있다" 고 표현하는 것이 맞다.
  • 컴파일 = 코드 생성만! => 작성한 타입스크립트가 유효하면 컴파일한다.

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

interface Square {
  width: number;
}

interface Rectangle extends Square {
  height: number;
}

type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
  if (shape instanceof Rectangle) {
    // ~~~~~~~~~ 'Rectangle'은 형식만 참조하지만 여기서는 값으로 사용되고 있습니다.
    return shape.width * shape.height;
    //         ~~~~~~~ 'Shape' 형식에 'height' 속성이 없습니다.
  } else {
    return shape.width * shape.width;
  }
}
  • instanceof 체크는 런타임에 일어나지만, Rectangle은 타입이기 때문에 런타임 시점에 아무런 역할을 할 수 없다.
    • 참고) instanceof는 클래스의 인스턴스를 확인할 때 사용한다.
  • 타입스크립트의 타입은 '제거 가능(eraseable)' 하다.

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

  • 속성 존재 확인

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

  • 태그 기법

    •   interface Square {
          kind: 'square'; // 런타임에 접근 가능한 타입 정보를 저장하는 태그!!
          width: number;
        }
      
        interface Rectangle {
          kind: 'rectangle';
          height: number;
          width: number;
        }
      
        type Shape = Square | Rectangle;
      
        function calculateArea(shape: Shape) {
          if (shape.kind === 'rectangle') {
            shape; // 타입이 Rectangle
            return shape.width * shape.height;
          } else {
            shape; // 타입이 Square
            return shape.width * shape.width;
          }
        }
    • Shape 타입은 태그된 유니온(tagged union) 의 한 예시이다.

    • 런타임에 타입 정보를 손쉽게 유지할 수 있기 때문에 흔하게 볼 수 있는 기법이다.

  • 클래스

    •   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; // 타입이 Rectangle
            return shape.width * shape.height;
          } else {
            shape; // 타입이 Square
            return shape.width * shape.width;
          }
        }
    • 타입(런타임 접근 불가)과 값(런타임 접근 가능)을 둘 다 사용하는 기법이 클래스이다.

    • 인터페이스는 타입으로만 사용 가능하지만, Rectangle을 클래스로 선언하면 타입과 값 모두 사용할 수 있다.

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

// string 또는 number 타입인 값을 항상 number로 정제하는 경우
function asNumber(val: number | string): number {
  return val as number;
}

function asNumber(val) {
  return val;
}

// 값을 정제하기 위해서는 런타임의 타입을 체크해야 하고 자바스크립트 연산을 통해 변환을 수행해야 한다.
function asNumber(val: number | string): number {
  return typeof val === 'string' ? Number(val) : val;
}
  • 위의 코드는 타입 체커를 통과하지만 잘못된 방법이다.
  • as number
    • 타입 단언문 (Type Assertion)
    • 타입 단언은 타입 연산이고 런타임 동작에는 아무런 영향을 미치지 않는다.

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

function setLightSwitch(value: boolean) {
  switch (value) {
    case true:
      turnLightOn();
      break;
    case false:
      turnLightOff();
      break;
    default:
      console.log('실행되지 않을까봐 걱정됩니다!');
  }
}
  • 타입스크립트는 일반적으로 실행되지 못하는 죽은(dead) 코드를 찾아낸다. 하지만 여기서는 strict를 설정하더라도 찾아내지 못한다.
  • 타입스크립트의 타입이기 때문에 :boolean은 런타임에서 제거된다. 그렇다면 자바스크립트였다면 실수로 메서드에 "ON"으로 호출할 수 있었을 것이다.
  • 순수 타입스크립트에서도 마지막 코드를 실행하는 방법이 있는데 대표적으로는 네트워크 호출로부터 받아온 값으로 함수를 실행하는 경우(API 응답)가 있다.
interface LightApiResponse {
  lightSwitchValue: boolean;
}

async function setLight() {
  const response = await fetch('/light');
  const result: LightApiResponse = await response.json();
  setLightSwitch(result.lightSwitchValue);
}
  • /light 요청을 하면 결과로 LightApiResponse를 반환하라고 선언했지만 실제로 그렇게 되리란 없음.
  • 만약 API를 잘못 파악해서 lightSwitchValue가 실제로는 문자열이었다면 런타임에는 setLightSwitch 함수까지 전달될 것이다.
  • 또는 배포된 후에 API가 변경되어 lightSwitchValue가 문자열이 되는 경우도 있을 것이다.
  • 타입스크립트에서는 런타임 타입과 선언된 타입이 맞지 않을 수 있다.
    • 따라서 선언된 타입이 언제든 달라질 수 있다는 것을 명심해야 한다.

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

function add(a: number, b: number) {
  //     ~~~ 중복된 함수 구현입니다.
  return a + b;
}

function add(a: string, b: string) {
  //     ~~~ 중복된 함수 구현입니다.
  return a + b;
}
  • 함수 오버로딩: 동일한 이름에 매개변수만 다른 여러 버전의 함수를 허용하는 것
  • C++ 같은 언어에서는 함수 오버로딩이 가능하다. 하지만 타입스크립트는 타입과 런타임의 동작이 무관하기 때문에 함수 오버로딩이 안된다.
  • 타입스크립트가 함수 오버로딩 기능을 지원하기는 하지만, 온전히 타입 수준에서만 동작한다.
function add(a: number, b: number): number;
function add(a: string, b: string): string;

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

const three = add(1, 2); // 타입이 number
const twelve = add('1', '2'); // 타입이 string
  • add에 대한 처음 두 개의 선언문은 타입 정보를 제공할 뿐이다.
  • 타입스크립트에서 자바스크립트로 변환될 때 위의 두 함수 선언문은 삭제되고 구현체만 남게된다.

타입스크립트 타입은 런타임 성능에 영향을 주지 않습니다.

  • 타입과 타입 연산자는 자바스크립트 변환 시점에 제거되기 때문에 런타임의 성능에 아무런 형향을 주지 않는다.
  • 타입스크립트의 정적 타입은 비용이 전혀 들지 않는다.

'런타임' 오버헤드가 없는 대신, 타입스크립트 컴파일러는 '빌드타임' 오버헤드가 있다.

  • 컴파일 속도가 상당히 빠른 편이고 특히 증분(incremental) 빌드 시에 더욱 체감된다.
  • 오버헤드가 커지면 빌드 도구에서 트랜스파일만(transpile only) 설정 (타입 체크를 건너뛴다)

호환성 높이고 성능 오버헤드 감안 vs 호환성 포기하고 성능 중심의 네이티브 구현체

  • 예를 들어 제너레이터 함수가 ES5 타깃으로 컴파일되려면 타입스크립트 컴파일러는 호환성을 위한 특정 헬퍼 코드를 추가할 것이다.
  • 이런 경우가 제너레이터의 호환성을 위한 오버헤드 또는 성능을 위한 네이티브 구현체 선택의 문제
  • 어떤 경우던 호환성과 성능 사이의 선택은 컴파일 타깃과 언어 레벨의 문제고 타입과는 무관하다.

요약정리🤔

- 코드 생성은 타입 시스템과 무관하다. 타입스크립트 타입은 런타임 동작이나 성능에 영향을 주지 않는다.
- 타입 오류가 존재하더라도 코드 생성(컴파일)은 가능하다.
- 타입스크립트 타입은 런타임에 사용할 수 없다. 런타임에 타입을 지정하려면, 타입 정보 유지를 위한 별도의 방법이 필요하다. 일반적으로는 태그된 유니온과 속성 체크 방법을 사용한다. 또는 클래스 같이 타입스크립트 타입과 런타임 값, 둘다 제공하는 방법이 있다.

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

덕 타이핑(duck typing)과 구조적 타이핑

덕 타이핑(duck typing)

  • 객체가 어떤 타입에 부합하는 변수와 메서드를 가질 경우 객체를 해당 타입에 속하는 것으로 간주하는 방식
  • "만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 그 새를 오리라고 부를 것이다."

  • 자바스크립트는 본질적으로 덕 타이핑(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); // 정상, 결과는 5
  • Vector2D와 NamedVector의 관계를 전혀 선언하지 않았다.
  • NamedVector를 위한 별도의 calculateLength를 구현할 필요도 없다.
  • 타입스크립트 타입 시스템은 자바스크립트의 런타임 동작을 모델링한다.
  • 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;
}

// 벡터의 길이를 1로 만드는 정규화 함수
function normalize(v: Vector3D) {
  const length = calculateLength(v); // ---- (1)
  return {
    x: v.x / length,
    y: v.y / length,
    z: v.z / length,
  };
}
  • 위의 예제에서 발생한 문제점은 normalize 함수가 1보다 조금 더 긴(1.41) 길이를 가진 결과를 출력한다는 것이다.
  • Vector2D를 받도록 선언된 calculateLength 메서드에서 Vector3D를 인수로 받았는데 타입 체커에서는 문제로 안본다. (1)
    • Vector3D로 calculateLength를 호출하면 구조적 타이핑 관점에서 x와 y가 있기 때문에 Vector2D와 호환된다.
    • 따라서 오류가 발생하지도 않았고 타입 체커가 문제로 인식하지 않았음.
  • 이처럼 구조적 타이핑의 문제점으로 타입 체커 및 런타임에서는 오류가 발생하지 않는데 의도와 다르게 동작하는 경우가 있다. 문제는 타입스크립트가 이 부분을 잡지 못한다는 것이다.

열려있는 타입 시스템

  • 함수를 작성할 때, 호출에 사용되는 매개변수의 속성들이 매개변수의 타입에 선언된 속성만을 가질 거라 생각하기 쉽다.
    • 이러한 타입은 봉인된(sealed) 또는 정확한(precise) 타입이라고 불림.
    • 타입스크립트 타입 시스템에서는 표현할 수 없다.
    • 타입은 항상 열려있다.
      • 열려있다 = 타입의 확장에 열려있다는 의미
      • 타입에 선언된 속성 외에 임의의 속성을 추가하더라도 오류가 발생하지 않는다는 것
      • ex) 고양이라는 타입에 크기 속성을 추가하여 작은 고양이가 되어도 고양이라는 사실은 변하지 않는다.
function calculateLengthL1(v: Vector3D) {
  let length = 0;
  for (const axis of Object.keys(v)) {
    const coord = v[axis];
    //            ~~~~~~~ 'string'은 'Vector3D'의 인덱스로 사용할 수 없기 때문에
    //           엘리먼트는 암시적으로 'any' 타입입니다.
    length += Math.abs(coord);
  }
  return length;
}

const vec3D = { x: 3, y: 4, z: 1, address: '123 Broadway' };
calculateLengthL1(vec3D); // 정상, NaN을 반환한다.
  • vec3D 변수는 Vector3D 라는 타입에 할당될 수 있다. x,y,z 모두 포함하고 있기 때문이다.
  • v는 어떤 속성이든 가질 수 있기 때문에 axis 타입은 string이 될 수도 있다. 그래서 타입스크립트는 v[axis]가 어떤 속성이 될지 알 수 없기 때문에 number라고 확정할 수 없다.
function calculateLengthL1(v: Vector3D) {
  // 루프보다는 모든 속성을 각각 더하는 구현이 더 낫다.
  return Math.abs(v.x) + Math.abs(v.y) + Math.abs(v.z);
}

구조적 타이핑 - 클래스

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

const c = new C('instance of C');
const d: C = { foo: 'object literal' }; // 정상
// d가 C에 왜 할당되는 것일까?
  • 이유1) d는 string 타입의 foo 속성을 가진다.
  • 이유2) 하나의 매개변수로 호출이 되는 생성자(Object.prototype)를 가진다.
    • 만약 C의 생성자에 단순 할당이 아니라 연산 로직이 존재하면 d의 경우는 생성자를 실행하지 않기 때문에 문제가 발생함!
    • 이런 부분이 C 타입의 매개변수를 선언하여 C 또는 서브클래스임을 보장하는 C++나 Java 같은 언어와 매우 다른 특징이다.
  • 구조적으로는 필요한 속성과 생성자가 존재하기 때문에 문제가 전혀 없다.

구조적 타이핑 - 테스트 작성

// 데이터베이스에 쿼리하고 결과를 처리하는 함수
interface Author {
  first: string;
  last: string;
}

function getAuthors(database: PostgresDB): Author[] {
  const authorRows = database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`);
  return authorRows.map((row) => ({ first: row[0], last: row[1] }));
}
  • getAuthors 함수를 테스트하기 위해서는 mocking한 PostgresDB를 생성해야 한다.
  • 하지만, 구조적 타이핑을 활용하여 더 구체적인 인터페이스를 정의하는 것이 나은 방법이다.
interface DB {
  runQuery: (sql: string) => any[];
}

// PostgresDB에서 DB로 추상화했음.
// PostgresDB에서만 사용할 수 있는 코드를 runQuery 메서드를 가지고 있는 모든 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] }));
}
  • runQuery 메서드가 있기 때문에 실제 환경에서도 getAuthors에 PostgresDB를 사용할 수 있다.
  • 구조적 타이핑 덕분에 PostgresDB가 DB 인터페이스를 구현하는지 명확히 선언할 필요가 없다.
test('getAuthors', () => {
  const authors = getAuthors({
    runQuery(sql: string) {
      return [
        ['Toni', 'Morrison'],
        ['Maya', 'Angelou'],
      ];
    },
  });
  expect(authors).toEqual([
    { first: 'Toni', last: 'Morrison' },
    { first: 'Maya', last: 'Angelou' },
  ]);
});
  • 타입스크립트는 테스트DB가 해당 인터페이스를 충족하는지 확인한다. 그리고 테스트 코드에는 실제 환경의 데이터베이스에 대한 정보가 불필요하다. + 모킹 라이브러리도 필요 없다.
  • 추상화(DB)를 하면서 로직과 테스트를 특정한 구현(PostgresDB)으로부터 분리한 것이다.
  • 구조적 타이핑의 또 다른 장점은 라이브러리 간의 의존성을 완벽하게 분리할 수 있다는 것이다.

요약정리🤔

- 자바스크립트가 덕 타이핑(duck typing) 기반이고 타입스크립트가 이를 모델링하기 위해 구조적 타이핑을 사용함을 이해해야 한다.
- 어떤 인터페이스에 할당 가능한 값이라면 타입 선언에 명시적으로 나열된 속성들을 가지고 있을 것이다. 타입은 '봉인'되어 있지 않고 열려있다.

- 클래스 역시 구조적 타이핑 규칙을 따른다는 것을 명심해야 한다. 클래스의 인스턴스가 예상과 다를 수 있다.

- 구조적 타이핑을 사용하면 유닛 테스팅을 손쉽게 할 수 있다.

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

타입스크립트의 타입 시스템은 점진적(gradual)이고 선택적(optional)이다.

  • 점진적: 코드에 타입을 조금씩 추가하는 것
  • 선택적: 언제든지 타입 체커를 해제할 수 있다는 것

위의 특성을 명백하게 가지고 있는 것이 any라는 타입이다. any를 사용하게 되면 타입스크립트의 수많은 장점을 누릴 수 없기 때문에 부득이하게 사용하더라도 위험성은 알고 사용하여야 한다.

any 타입을 권장하지 않는 이유는?

권장하지 않는 이유는 크게 6가지가 있다.

  • 타입 안전성이 없다 ==> 어떤 타입도 할당할 수 있다

    • let age: number; age = '12' as any; // any로 선언하게 되면 number가 아닌 string도 할당이 가능하다! age += 1; // 런타임에 정상, age는 "121"
  • 함수 시그니처(contract)를 무시해버린다

    •   function calculateAge(birthDate: Date): number {
          ...
        }
      
        let birthDate: any = "1990-01-19";   // 타입을 any로 선언했기 때문에 에러는 발생하지 않는다.
        calculateAge(birthDate); // 정상
  • 언어 서비스가 적용되지 않는다 (자동완성 등 in IDE)

  • 코드 리팩터링 때 버그를 감춘다

    • 타입 체커를 통과해도 런타임에서 오류가 발생할 수 있다.
    • any가 아니라 구체적인 타입을 적용한다면 분명히 오류를 발견할 수 있다!
  • 타입 설계를 감춰버린다

    • 객체를 정의할 때 any를 사용하게 되면 상태 객체의 설계를 감춰버리기 때문에 특히 문제가 된다.
    • 동료가 코드를 검토한다면 동료는 애플리케이션의 상태를 어떻게 변경했는지 코드부터 재구성해봐야 하기 때문에 설계가 명확히 보이도록 타입을 일일이 작성해주는 것이 좋다.
  • 타입시스템의 신뢰도를 떨어뜨린다

    • 보통은 타입 체커가 실수를 잡아주고 코드의 신뢰도가 높아진다. 하지만 런타임에 타입 오류를 발견하게 되다면 타입 체커를 신뢰할 수 없을 것이다.
    • any 타입을 쓰지 말아야 런타임에 발견될 오류를 미리 잡을 수 있고 신뢰도를 높일 수 있다.

요약정리🤔

- any 타입을 사용하게 되면 타입 체커와 타입스크립트 언어를 무력화한다.

- 진짜 문제점을 감추고 개발 경험을 나쁘게 하고 타입 시스템의 신뢰도를 떨어뜨린다.

- 그냥 쓰지말자 웬만하면!