🎯 TS는 타입 애너테이션 방식

타입스크립트는 타입 애너테이션 방식을 사용하는데 이는 변수나 상수 혹은 함수의 인자와 반환 값에 타입을 명시적으로 선언하여 어떤 타입 값이 저장될 것인지를 컴파일러에 직접 알려주는 문법이다.

let decimal: number = 6;
let color: string = 'blue';

사실 위의 예시에서 : type 선언부를 제거해도 코드는 정상적으로 동작한다. 하지만 타입을 제거하면 타입스크립트 타입 시스템이 타입 추론을 하는 과정에서 어려움을 겪을 것이다.

🎯 TS는 구조적 타이핑이다

다른 언어와는 다르게 타입스크립트에서 타입을 구분하는 방식은 조금 다르다. 타입스크립트는 구조로 타입을 구분하는데 이것을 구조적 타이핑이라고 한다. 이를 예시와 함께 살펴보자.

interface Pet {
  name: string;
}

let cat = { name: 'Kitty', age: 2 };

function greet(pet: Pet) {
  console.log('Hello, ' + pet.name);
}

greet(cat); // ✅

몹시 당황스러운 코드이다. 분명 greet이라는 함수의 매개변수로 들어갈 수 있는 값은 Pet 타입으로 제한되어 있다. 그러나 타입을 따로 명시하지 않은 cat 객체를 greet 함수의 인자로 전달해도 코드는 정상적으로 실행된다.

이유는 cat 객체는 Pet 인터페이스가 가지고 있는 name 속성을 가지고 있어 pet.name의 방식으로 name 속성에 접근할 수 있기 때문이다.

위와 같은 타이핑 방식이 구조적 타이핑이다. 타입의 상속 역시 구조적 타이핑을 기반으로 하고 있다.

🎯 구조적 타이핑의 단점

타입스크립트 구조적 타이핑의 특징 때문에 예기치 못한 결과가 나올 때도 있다. 그 예시가 바로 아래 코드이다.

interface Cube {
  width: number;
  height: number;
  depth: number;
}

function addLines(c: Cube) {
  let total = 0;

  for (const axis of Object.keys(c)) {
    // ❌ ❌ ❌
    const length = c[axis];

    total += length;
  }
}

addLines() 함수의 매개변수인 c는 Cube 타입으로 선언되었고, Cube 인터페이스의 모든 필드는 number 타입을 가지기 때문에 c[axis]는 당연히 number 타입일 것이라고 예측할 수 있다. 그러나 c에 들어올 객체는 Cube가 기존에 가진 속성 외에도 이외에 속성을 가진 객체가 들어올 수도 있기 때문에 에러가 발생한다. 즉 아래와 같은 상황이다.

interface namedCube {
  width: 6;
  height: 5;
  depth: 4;
  name: 'Sweet'; // string 타입의 추가 속성이 정의되었다.
}

addLines(namedCube); // ✅

🎯 TS의 점진적 타입 확인

타입스크립트는 점진적으로 타입을 확인하는 언어다. 점진적 타입 검사란 컴파일 타임에 타입을 검사하면서 필요에 따라 타입 선언 생략을 허용하는 방식이다. 타입 선언을 생략하면 암시적 타입 변환이 일어난다.

function add(x, y) {
  return x + y;
}

function add(x: any, y: any): any;

그러나 이러한 특징 때문에 타입스크립트의 타입 시스템은 정적 타입의 정확성을 100% 보장해주지 않는다. 모든 변수와 표현식의 타입을 컴파일타임에 검사하지 않아도 되기 때문에 타입이 올바르게 정해지지 않으면 런타임에서 에러가 발생하기도 한다.

💎 any 타입

타입스크립트에서 any 타입은 타입스크립트 내 모든 타입의 종류를 포함하는 가장 상위 타입으로 어떤 타입 값이든 할당할 수 있다.

🎯 값과 타입을 혼용하지 말자

타입은 우리가 흔히 아는 number, boolean, string 타입 외에도 type이나 interface 키워드로 커스텀 타입을 정의할 수도 있다.

type Person = {
  name: string;
  age: number;
};

interface Person {
  name: string;
  age: number;
}

값 공간과 타입 공간의 이름은 서로 충돌하지 않기 때문에 타입과 변수를 같은 이름으로 정의할 수 있는데 타입스크립트가 자바스크립트의 슈퍼셋인 것과 관련이 있다. 타입스크립트 문법인 type으로 선언한 내용은 자바스크립트 런타임에서 제거되기 때문에 값 공간과 타입 공간은 서로 충돌하지 않는다.

type Developer = { isWorking: true };
const Developer = { isTyping: true };

하지만 타입스크립트에서 값과 타입의 구분은 맥락에 따라 달라지기 때문에 값 공간과 타입 공간을 혼동할 때도 있다.

function email(options: { num: number; subject: string; body: string }) {
  // ....
}

function email({ num: number, subject: string, body: string }) {
  // ....
}

의도는 이해가 가지만 위와 같이 작성하면 오류가 발생한다. 특히 두번째 코드에 등장하는 구조분해 할당은 타입스크립트에서 오류가 발생한다. 하나의 key와 value로 인식해버리기 때문이다.

위와 같은 문제를 해결하기 위해서는 값과 타입을 구분해서 작성해야 한다.

function email({ num, subject, body }: { num: number; subject: string; body: string }) {
  // ...
}

💎 TS에서 클래스는 타입으로도 사용된다

class Developer {
  name: string;
  domain: string;

  constructor(name: string, domain: string) {
    this.name = name;
    this.domain = domain;
  }
}

const me: Developer = new Developer('zig', 'frontend');

변수명 me 뒤에 등장하는 Developer에서 Developer는 타입에 해당하지만 new 키워드 뒤의 Developer는 클래스 생성자 함수인 값으로 동작한다.

타입스크립트에서 클래스는 타입 애너테이션으로 사용할 수 있지만 런타임에서 객체로 변환되어 자바스크립트의 값으로 사용되는 특징이 있다.

🎯 타입을 확인하는 방법

타입스크립트에서는 값 공간과 타입 공간이 별도로 존재한다. 타입스크립트에서 typeof 연산자도 값에서 쓰일 때와 타입에서 쓰일 때의 역할이 다르다.

class Developer {
  name: string;
  sleepingTime: number;

  constructor(name: string, sleepingTime: number) {
    this.name = name;
    this.sleepingTime = sleepingTime;
  }
}

const d = typeof Developer; // 'function'
type T = typeof Developer; // typeof Developer

자바스크립트의 클래스는 결국 함수이기 때문에 값 공간에서는 function이 된다. 타입 공간에서의 반환 값은 그 자체인 typeof Developer가 된다.

💎 타입 단언: as

타입스크립트에서는 타입 단언 이라 부르는 문법을 사용해서 타입을 강제할 수도 있는데 이는 as키워드를 사용하면 된다. 타입 단언은 개발자가 해당 값의 타입을 더 잘 파악할 수 있을 때 사용되며 강제 형 변환과 유사한 기능을 제공한다.

const loaded_text: unknown; // unknown 타입 값을 전달 받았다는 가정

const validateInputText = (text: string) => {
  if (text.length < 10) return '최소 10글자 이상 입력해주세요';
  return '정상 입력된 값입니다.';
};

validateInputText(loaded_text as string);

위 코드에서는 as 키워드를 사용해서 string 타입으로 강제하지 않으면 타입스크립트 컴파일러 단계에서 에러가 발생한다.