성장, 그리고 노력

부족하더라도 어제보다 더 잘해지자. 노력은 절대 배신하지 않는다.

Typescript

[Typescript] 타입 시스템

제이콥(JACOB) 2019. 12. 21. 12:25

(너무 쉽거나 기본적인 내용은 맨 앞 글에 기본 문법을 참고하시거나 다른 글 or책을 참고 부탁드립니다.)

 

 타입스크립트의 타입 시스템은 "왜 타입스크립트를 사용해야 하는가?"에 대한 적절한 대답이다. 타입 시스템은 놀라울 정도로 강력하고, 우리가 예측하지 못했던 많은 것들을 표현할 수 있게 해 준다. 여기에 대해 좀 더 자세히 알아보자.


"타입 시스템은 집합의 개념이다"

위 문장을 지금 당장 이해하지 못해도 좋다. 하지만 집합의 개념에서 타입 시스템을 바라보지 않는다면, 기본은 알아도 활용할 때에 바로 한계에 부딪칠 가능성이 크다. 아래 글을 읽으면서 모르는 문제가 나왔을 때 집합의 개념에서 접근해 보자. 

extend

아래 코드를 먼저 살펴보자.

function getKey<K extends string>(value: any, key: K) {
    console.log(value, key);
}

 여기서 extends string이 의미하는 것은 무엇일까? 이것을 설명할 수 있다면 이 글은 넘겨도 된다. 하지만, 아니라면 곰곰히 생각 후에 아래 내용을 읽어보자. 

 만약 "상속"의 의미로 위 문제에 대한 답을 접근했다면, 매우 복잡한 논리가 머리속에서 그려져야 설명이 가능할 것이다. 이런 논리라면 굳이 상속을 하지 않고 string을 딱 정해도 되는 일이 아닌가? 

 그렇다면 editor를 사용해서 도움 메시지를 참고해 보자.

Typescript의 절친 vscode!

keystring이라고 알려주고 있다. 

그렇다면 집합의 개념으로 다가가보자. extend하위 집합의 개념이다. A extends B라고 한다면, A는 B의 하위 집합인 거다. K는 string의 하위 집합이다. 따라서 K는 string 타입이 가질 수 있는 것들을 가질 수 있다. string 자체 뿐만 아니라 string literal types, unions of string literal types도 해당되는 것이다.

keyof

 keyof는 하나의 object의 key들을 타입으로 반환해 준다.

type PointKeys = keyof Point; // type PointKeys === "x" | "y"

function sortBy<K extends keyof T, T>(inputArray: T[], key: K): T[] {
    // ...
    return inputArray;
}
const inputArray: Point[] = [{x: 10, y: 20}, {x: 3, y: 7}];

keyof와 extends를 같이 사용했지만, 사용된 의미나 방법은 위에 예제랑 다를 게 없다.

좀 더 확실하게 보기 위해, editor을 사용해 봤다. 

key의 경우 keyof T 즉, x | y만 올 수 있다는 것을 보여준다. 글을 얼마 작성하지 않았지만, 벌써 타입스크립트의 놀라운 능력들을 알게 되는 기분이다. 

 

Array와 Tuple은 다르다

const array = [1, 2]; // type is number[]

const tuple: [number, number] = array; // error!

// error TS2739: Type 'number[]' is missing the 
// following properties from type '[number, number]': 0, 1

위 코드를 실행해 보면 에러가 발생한다. 

언뜻보면 두 개다 숫자 배열 타입으로 보이지만, number []와 [number, number]은 다르기 때문에 할당 시에 에러가 난다. 좀 더 자세히 표현하자면, 타입스크립트의 타입 시스템에서는 number []는 숫자의 배열이지만, [number, number]은 숫자 타입의 튜플인 것이다. 하위 집합의 개념을 적용할 수 없으므로 당연히 할당도 불가능한 것이다. 

 

 

과한 프로퍼티(Excess Property) 검사의 필요성

아래의 코드를 예측해 보자. 타입스크립트를 사용한다면 아래의 코드를 오류를 반환할까?

interface Person { // Person 인터페이스
  name: string;
  age: number;
}

const Person = { // Person 객체
  name: "jacob",
  age: 28,
  address: "seoul"
};

const a: Person = Person;

 정답은 아무 이상없다. 이유는 Person 객체의 타입은 {name: string, age: number, address: "seoul"}로 추론(infer)된다. Person 객체는 Person 인터페이스의 하위 집합이기 때문에 Person 객체를 a에 할당해도 문제가 없다.

 이것은 과한 프로퍼티 검사가 필요한 이유이다. 원치 않는 프로퍼티를 가진 객체가 Person인마냥 마음대로 돌아다닐 수 있다면, 개발자는 Person객체가 가진 타입을 신뢰할 수도 없고 어떤 프로터피가 있는지 알 수 없을 것이다.

 

다른 예시를 하나 더 살펴보자.

interface Options {
    title: string;
    darkMode?: boolean;
}

function createWindow(options: Options) {
    if (options.darkMode) {
        console.log('다크모드');
    } else {
        console.log('밝은 모드');
    }
}

createWindow({
    title: '다크모드 할래요',
    darkModee: true, // Typo  - 1
})

const obj = {
    title: '다크모드 할래요',
    darkModee: true, // Typo
};

createWindow(obj); // - 2

오타가 나서 잘못 작성한 코드가 있다. 하지만 타입스크립트를 사용했어도 1번은 바로 오류가 확인되지만, 2번은 런타임 때가 되어야 확인이 가능할 것이다. 

19번째 줄을 보면 오타가 있는 객체(obj)를 24번째 줄에서 인자 값으로 넘겼지만 에러가 나지 않는다. 타입 시스템에서는 obj를 obj literal로 인식해서 하위 집합으로 여기고 할당할 수 있게 되어버리는 것이다. 그렇다면 과한 프로퍼티 검사를 해보자. 

내가 생성한 obj에 명시적으로 "넌 무슨 타입이야!"라고만 전달해 주면된다. 위에 있는 Person 예제도 같은 논리이니, 실제로 한번 해보자.

 

모든 함수의 인자와 리턴값에도 타입을 적용하자.

타입 체크의 중요성은 이미 위에서 많이 언급해서 알고 있을 것이다. 그렇다면 이제 함수에도 적용해 보자.

 초록 밑줄은 함수의 인자에 타입을 지정해 준 것이고, 빨간 밑줄은 리턴 값의 타입을 지정한 것이다. 가장 기본적이고 일반적인 타입이다. 그런데 같은 형식의 타입 지정을 반복적으로 많이 해야하는 경우도 있다.

 한 가지 타입 형식이 반복된다면, 화살표 함수를 이용해서 타입을 따로 분리해서 작성해도 된다.

 

반응형