타입 추론(Tpye Inference)

타입 추론이란, 타입스크립트가 코드를 해석해 나가는 동작을 의미한다.

let x = 3;
let y = 'a';

console.log(typeof x); // number
console.log(typeof y); // string

위와 같이 xy에 타입을 따로 지정하지 않더라도 x는 number로 y는 string 타입으로 간주한다. 변수를 선언하거나 초기화 단계에서 타입이 추론된다. 이외에도 변수, 속성, 인자의 기본 값, 함수의 반환 값 등을 설정할 때 타입 추론이 일어난다.

Best Common Type

타입은 보통 몇 개의 표현식을 바탕으로 타입을 추론한다. 해당 표현식을 이용하여 가장 근접한 타입을 추론하는데 이 때 가장 근전합 타입을 Best Common Type이라고 한다.

le arr = [0, 1, null];

위 변수 arr의 타입을 추론하기 위해서는 배열의 값들을 살펴봐야 한다. 배열안에 있는 값들은 numbernull 타입으로 구분된다. 이 때 Best Common Type 알고리즘으로 다른 타입들과 가장 잘 호환되는 타입을 선정한다.


문맥에 맞는 타이핑(Contextual Typing)

타입스크립트는 경우에 따라 또 다른 방식으로 타입을 결정한다. 이것을 컨텍스트 타이핑이라고 한다. 이 컨텍스트 타이핑은 코드 위치를 기준으로 결정한다.

window.onmousedown = function (mouseEvent) {
  console.log(mouseEvent.button);
  console.log(mouseEvent.kangaroo);
// Property 'kangaroo' does not exist on type 'MouseEvent'.
};

위 코드를 타입스크립트 검사기 관점으로 본다면 window.onmousedown에 할당되는 함수의 타입을 추론하기 위해서 window.onmousedown 타입을 검사한다. 타입 검사가 끝나면 함수의 타입이 마우스 이벤트와 연관 있다고 추론하기 때문에 mouseEvent 인자에 button 속성은 존재하지만 kangaroo 속성은 없다고 결론을 내린다.


window.onscroll = function(uiEvent) {
  console.log(uiEvent.button);
// Property 'button' does not exist on type 'Event'.
}

위의 다른 예제를 살펴보면 타입스크립트는 오른쪽 함수는 window.onscroll에 할당되었기 때문에 uiEventUIEvent로 알고 있다. 따라서, 이전 예제와 같이 MouseEvent와 다르게 button 속성이 없다고 추론하기 때문에 속성이 없다고 결론내리는 것이다.


실제로 uiEvent에는 어떤 속성들이 있는지 확인해보자. 크롬 콘솔창에 window를 입력하면 아래와같이 전역객체의 속성들이 쭈~욱 나온다. 거기서 onscroll을 보면 null로 되어있을 것이다.

window // 크롬 콘솔에 입력

image


그리고 아래와 같이 입력 후 마우스 스크롤을 움직여 보면 아래와 같이 속성값들을 확인할 수 있다.

window.onscroll = function(uiEvent) {
  console.log(uiEvent);
// Property 'button' does not exist on type 'Event'.
}

image


컨텍스트 타이핑을 더 이해하기 위해서는 아래와 같이 코드를 바꿔서 확인해보자.

const handler = function(uiEvent){
  console.log(uiEvent.button);
}

위의 예제코드와 동일하지만, 함수에 할당되는 변수만으로는 타입을 추론하기 어렵다. 그렇기 때문에 타입스크립트는 에러로 발생시키지 않는다.

참고!
현재 uiEvent 인자에 타입을 지정하지 않아도 에러가 발생하지 않는다. 단, 타입스크립트 config에 –noImplicitAny 옵션을 true로 지정하면 에러가 발생한다.


타입스크립트의 타입 체킹

타입 체킹에 있어 타입스크립트의 지향점은 타입 체크는 값의 형태에 기반하여 이루어져야 한다는 점이다.
이것을 Duck Typing 또는 Structural Subtyping이라고 한다.

  • Duck Typing: 객체의 변수 및 메서드의 집합이 객체의 타입을 결정하는 것을 의미한다. 동적 타이핑의 한 종류
  • Structural Subtyping: 객체의 실제 구조나 정의에 따라 타입을 결정하는 것을 의미한다.


타입 호환(Type Compatibility)

타입 호환이란 타입스크립트에서 특정 타입이 다른 타입에 잘 맞는지를 의미한다.

예를 들어 아래와 같은 코드를 의미한다.

interface Ironman {
  name: string;
}

class Avengers {
    name = 'string'
}

let i: Ironman;
i = new Avengers(); // OK, because of structural typing


console.log(i.name)

C#, java와 같은 코드였다면 에러가 발생했을 것이다. 그 이유는 Avengers 클래스가 명시적으로 Ironman 인터페이스를 상속받은게 아니기 때문이다.

하지만, 자바스크립트는 정상적으로 동작한다.

그이유는, 자바스크립트의 작동 방식과 관련이 있다. 기본적으로 자바스크립트는 객체 리터럴이나 익명 함수 등을 사용하기 때문에 명시적으로 타입을 지정는 것보다는 코드의 구조 관점에서 타입울 지정하는 것이 잘 어울린다.

구조적 타이핑

구조적 타이핑이란(structural typing)이란 코드 구조 관점에서 타입이 서로 호환되는지 여부를 판단하는 것이다.

아래 코드를 보자.

interface Avengers {
  name: string;
}

let hero: Avengers;
// 타입스크립트가 추론한 y의 타입은 { name: string; location: string; } 입니다.
let capt = { name: "Captain", location: "Pangyo" };
hero = capt;

위 코드에서 capt가 hero 타입에 호환될 수 있는 이유는 capt 속성 중에 name이 있기 때문이다. 만약에 name이 없다면 타입에러를 발생시킨다. 즉, Avengers 인터페이스에서 name 속성 값을 가지고 있기 때문에 capt는 Avengers에 호환될 수 있던 것이다.

함수를 호출할 때도 마찬가지로 작동한다.

interface Avengers {
  name: string;
}

// capt 변수 타입은 { name: string; location: string; }
let capt = { name: "Captain", location: "Pangyo" };

function assemble(a: Avengers) {
  console.log("어벤져스 모여라", a.name);
}
assemble(capt); // 어벤져스 모여라 Captain


Soundness

타입스크립트는 컴파일 시점에서 타입을 추론할 수 없는 특정 타입에 경우는 일단 안전하다고 보는 특성이 있다. 이걸 "들리지 않는다(it is said to not be sound)"라고 표현한다.


Enum 타입 호환시 주의

이넘 타입은 number 타입과 호환되지만 이넘 타입끼리는 호환되지 않는다.

enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };

let ready = Status.Ready
ready = Color.Red // Error


Class 타입 호환시 주의

클래스 타입은 클래스 타입끼리 비교할 때 스태틱 멤버(static member)와 생성자(constructor)를 제외하고 속성만 비교한다.

class Hulk {
  handSize: number;
  constructor(name: string, numHand: number) { }
}

class Captain {
  handSize: number;
  constructor(numHand: number) { }
}

class Iron {
  footSize : number;
  constructor(numHand: number) { }
}

let hulk: Hulk;
let capt: Captain;
let iron : Iron;

hulk = capt; // ok
capt = hulk; // ok
iron = hulk; // 'footSize' 속성이 'Hulk' 형식에 없지만 'Iron' 형식에서 필수입니다.

Hulk와 Captain은 생성자는 다르지만 속성은 똑같기 때문에 호환에 문제가 없다. 하지만, Iron과 Captaindms 생성자는 동일하지만 속성은 다르기 때문에 오류가 발생한다.


Generics

제네릭은 제네릭 타입 간의 호환 여부를 판단할 때 타입 인자 <T>가 속성에 할당 되었는지를 기준으로 판단한다.

interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;

x = y;  // OK, because y matches structure of x

위의 인터페이스는 속성(member 변수)가 없기 때문에 x와 y를 같은 타입으로 간주한다.
하지만 아래와 같이 인터페이스에 속성이 있다면 제네릭의 타입 인자가 속성에 할당되기 때문에 x와 y는 서로 다른 타입으로 간주된다.

interface Empty<T> {
  data : T;
}
let x: Empty<number>;
let y: Empty<string>;

x = y;  // 'Empty<string>' 형식은 'Empty<number>' 형식에 할당할 수 없습니다.
        // 'string' 형식은 'number' 형식에 할당할 수 없습니다


타입 별칭(Type Aliases)

타입 별칭은 특정 타입이나 인터페이스를 참조할 수 있는 타입 변수를 의미한다.

// string 타입을 사용할 때
const name: string = 'siksik';

// 타입 별칭을 사용할 때
type MyName = string;
const name: MyName = 'siksik';

간단한 타입 뿐만 아니라 interface 레벨의 복잡한 타입에서도 별칭을 부여할 수 있다.

type intro = {
  name: string;
  address: string;
  age: number;
}

const who: intro = {
  name: 'siksik',
  address: 'seoul',
  age: 20,
}

타입 별칭의 특징

타입 별칭을 사용하는 이유는, 타입 값을 새로 생성하는 의미보다는 별칭을 부여하여 나중에 쉽게 참고할 수 있도록 이름을 사용하는 것이다.
이러한 특징이 VSCode 상의 프리뷰 상태로 다른 타입과 어떤 차이점이 있는지 확인해볼 수 있다.

image

image

type vs interface

타입 별칭과 인터페이스의 가장 큰 차이는 타입의 확장 여부(가능/불가능)이다.
인터페이스는 확장이 가능한 반면 타입 별칭은 확장이 불가능하다. 따라서, 가능하면 type 보다는 interface를 사용하는 것을 추천한다.