TypeScript 이해부터 하고 사용하자..

TypeScript 이해부터 하고 사용하자..

타입스크립트를 왜 사용할까 ? 어떻게 하면 더 잘 사용할 수 있을까 ?

·

13 min read

🫠 과연 나는 TypeScript 를 잘 쓰고 있는 걸까 ?

최근 진행했던 기업의 기술 과제에서 받았던 피드백 중에 타입스크립트 관련 피드백을 많이 받았다 💬

이전부터 타입스크립트를 왜 사용하고 어떤점이 효과적인지 필요성에 대해서는 많이 들어보았고 나름대로 잘 사용하고 있다고 생각했었는데 그렇지 않았던 것 같다 😂

🙄 타입스크립트를 왜 사용하는지, 어떻게 하면 더 잘 사용할 수 있는지에 대해 고민해보시는 것도 석진님의 성장에 도움이 될 거라고 생각합니다. 타입스크립트가 어떻게 안전한 코드를 작성하는데 도움이 되는지, 타입으로 구체적인 값만을 허용하려면 어떻게 해야하는지, 코드 작성자의 의도를 드러내려면 어떻게 해야하는지 고민해보시는 것을 조심스럽게 추천드립니다.

이번 기회에 작성했던 코드를 보면서 피드백 받은 내용들을 참고해보면서 수정을 진행해보려고 한다.

  1. 타입스크립트를 왜 사용하는지 ❓

  2. 타입스크립트가 어떻게 안전한 코드를 작성하는데 도움이 되는지 ❓

  3. 타입으로 구체적인 값 만을 허용하려면 어떻게 해야하는지 ❓

  4. 코드 작성자의 의도를 드러내려면 어떻게 해야하는지 ❓

1️⃣ 타입스크립트는 왜 사용하는 걸까

첫째, 정적 타입을 지원하여 코드의 가독성과 유지 보수성을 높일 수 있습니다.

둘째, 컨파일 타임에 오류를 검출할 수 있으므로 런타임 에러를 줄이고 생산성을 높일 수 있습니다.

셋째, 타입 정보를 활용하여 코드 에디터의 자동 완성, 리팩토링, 오류 강조 등 강력한 개발 환경을 제공한다.

🤔 그렇다면 타입스크립트를 사용하면 항상 오류를 검출하고 에러를 방지할 수 있는걸까 ?

✅ 그에 대한 대답은 타입스크립트 컴파일러가 수행하는 역할을 되짚어 보면, 타입스크립트가 할 수 있는 일과 할 수 없는 일을 짐작할 수 있다.

큰 그림에서 보면, 타입스크립트 컴파일러는 두 가지 역할을 수행합니다.

  1. 최신 타입스크립트 / 자바스크립트를 브라우저에서 동작할 수 있도록 구버전의 자바스크립트로 트랜스파일(transpile) 합니다.

  2. 코드의 타입 오류를 체크합니다.

여기서 놀라운 점은 이 두 가지가 서로 완벽히 독립적 이라는 것인데, 타입스크립트가 자바스크립트로 변환될 때 코드 내의 타입에는 영향을 주지 않는다. 또한 그 자바스크립트의 실행 시점에도 타입은 영향을 미치지 않는다.

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

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

$ cat test.ts
let x = "hello";
x = 1234;
$ tsc test.ts
test.ts:2:1 - error TS2322: "1234" 형식은 "string" 형식에 할당할 수 없습니다.

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

예를 들어 다음과 같은 코드를 통해 알아보면,

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 은 타입이기 때문에 런타임 시점에 아무런 역할을 할 수 없습니다. 타입스크립트의 타입은 ‘제거 가능(erasable)’ 합니다. 실제로 자바스크립트로 컴파일되는 과정에서 모든 인터페이스, 타입, 타입 구문은 그냥 제거되어 버립니다.

앞의 코드에서 다루고 있는 shape 타입을 명확하게 하려면, 런타임에 타입정보를 유지하는 방법이 필요합니다.

하나의 방법은 height 속성이 존재하는지 체크하는 것입니다.

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

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';
    width: number;
    height: 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)’의 한 예입니다. 이 기법은 런타임에 타입 정보를 손쉽게 유지할 수 있기 때문에, 타입스크립트에서 흔하게 볼 수 있습니다.

타입(런타임 접근 불가)과 값(런타임 접근 가능)을 둘 다 사용하는 기법도 있습니다. 타입을 클래스로 만들면 됩니다.SquareRectangle을 클래스로 만들면 오류를 해결할 수 있습니다.

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;
        return shape.width * shape.height;
    } else {
        shape;
        return shape.width * shape.width;
    }
}

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

type Shape = Square | Rectangle 부분에서 Rectangle은 타입으로 참조되지만, shape instanceof Rectangle 부분에서는 값으로 참조됩니다.

요약

  1. 코드 생성은 타입 시스템과 무관합니다. 타입스크립트 타입은 런타임 동작이나 성능에 영향을 주지 않습니다.

  2. 타입 오류가 존재하더라도 코드 생성(컴파일)은 가능합니다.

  3. 타입스크립트 타입은 런타임에 사용할 수 없습니다. 런타임에 타입을 지정하려면, 타입 정보 유지를 위한 별도의 방법이 필요합니다. 일반적으로는 태그된 유니온과 속성 체크 방법을 사용합니다. 또는 클래스 같이 타입스크립트 타입과 런타임 값, 둘 다 제공하는 방법이 있습니다.


2️⃣ 타입스크립트가 어떻게 안전한 코드를 작성하는데 도움이 될까 ❓

가장 먼저 떠오르는 방법으로는 엄격한 옵션을 설정하는 것으로 안전한 코드를 작성하는 것에 도움이 될 거라고 생각하는데 그럴 경우 주의해야 하는 것이 있습니다.

🚨 any 타입 지양하기

타입스크립트의 타입 시스템은 점진적(gradual)이고 선택적(optional)입니다. 코드에 타입을 조금씩 추가할 수 있기 때문에 점진적이며, 언제든지 타입 체커를 해제할 수 있기 때문에 선택적입니다. 이 기능들의 핵심은 **any** 타입입니다.

let age: number; 
age = "12";
// 'string' 형식은 'number' 형식에 할당할 수 없습니다.
age = "12" as any;

타입 선언을 추가하는 데에 시간을 쏟고 싶지 않아서 **any** 타입이나 타입 단언문(**as any**)를 사용하고 싶을 수 있습니다. 그러나 일부 특별한 경우를 제외하고는 **any**를 사용하면 타입스크립트의 수많은 장점을 누릴 수 없게 됩니다. 부득이하게 any를 사용하더라도 그 위험성을 알고 있어야 합니다.

any 타입에는 타입 안정성이 없습니다

코드에서 age는 number 타입으로 선언되었습니다. 하지만 as any를 사용하면 string 타입을 할당할 수 있게 됩니다. 타입 체커는 number 타입으로 판단할 것이고 코드는 정상적으로 동작하지 않을 것입니다.

age += 1; // 런타임에 정상, 그러나 age는 "121"

any는 함수 시그니처를 무시해 버립니다

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

function calculateAge(birthDate: Date): number {
    return birthDate.getFullYear();
}

// 'string' 형식의 인수는 'Date' 형식의 매개 변수에 할당될 수 없습니다.
// let birthDate = '1998-09-09';
// calculateAge(birthDate);

let birthDate: any = '1998-09-09';
calculateAge(birthDate); // 정상

any는 타입시스템의 신뢰도를 떨어뜨립니다

타입스크립트는 개발자의 삶을 편하게 하는 데 목적이 있지만, 코드 내에 존재하는 수많은 any 타입으로 인해 자바스크립트보다 일을 더 어렵게 만들기도 합니다. 타입 오류를 고쳐야 하고 여전히 머릿속에 실제 타입을 기억해야 하기 때문입니다. 타입이 실제 값과 일치한다면 타입 정보를 기억해 둘 필요가 없습니다. 타입스크립트가 타입 정보를 기억해 주기 때문입니다.

요약

any 타입을 사용하면 타입 체커와 타입스크립트 언어 서비스를 무력화시켜 버립니다. any 타입은 진짜 문제점을 감추며, 개발 경험을 나쁘게 하고, 타입 시스템의 신뢰도를 떨어뜨립니다. 최대한 사용을 피하도록 합시다.

3️⃣ 타입으로 구체적인 값 만을 허용하려면 어떻게 해야할까 ❓

“구체적인 값 만을 허용시킨다” 라는 표현을 이해하려면 타입스크립트에서 사용하는 타입에 대해서 이해하고 필요한 상황에 적절한 타입을 선언할 줄 알아야 한다.

타입이 값들의 집합이라고 생각하기

집합이란 ? : 어떤 명확한 조건을 만족시키는 서로 다른 대상들의 모임

런타임에 모든 변수는 자바스크립트 세상의 값으로부터 정해지는 각자의 고유한 값을 가집니다. 변수에는 다음처럼 다양한 종류의 값을 할당할 수 있습니다.

  • 42

  • null

  • undefined

  • new HTMLButtonelement

  • ‘Canada’

  • /regex/

  • { animal: ‘Whale’, weight_lbs: 40_000 }

  • (x, y) ⇒ x + y

그러나 코드가 실행되기 전, 즉 타입스크립트가 오류를 체크하는 순간에는 ‘타입’을 가지고 있습니다. ‘할당 가능한 값들의 집합’이 타입이라고 생각하면 됩니다.

이 집한은 타입의 ‘범위’라고 부르기도 합니다. 예를 들어, 모든 숫자값의 집합을 number 타입이라고 생각할 수 있습니다. 42와 37.25는 number 타입에 해당되고, ‘Canada’는 그렇지 않습니다. nullundefinedstrictNullChecks 여부에 따라 number에 해당될 수도, 아닐 수도 있습니다.

가장 작은 집합은 아무 값도 포함하지 않는 공집합이며, 타입스크립트에서는 never 타입입니다. never 타입으로 선언된 변수의 범위는 공집합이기 때문에 아무런 값도 할당할 수 없습니다.

const x:never = 12;
// 'number' 형식은 'never' 형식에 할당할 수 없습니다.

그 다음으로 작은 집합은 한 가지 값만 포함하는 타입입니다. 이들은 타입스크립트에서 유닛(unit) 타입이라고도 불리는 리터럴(literal) 타입입니다.

type A = 'A';
type B = 'B';
type Twelve = 12;

두 개 혹은 세 개로 묶으려면 유니온(union) 타입을 사용합니다.

type AB = 'A' | 'B';
type AB12 = 'A' | 'B' | 12;

세 개 이상의 타입을 묶을 때도 동일하게 | 로 이어주면 됩니다. 유니온 타입은 값 집합들의 합집합을 일컫습니다.

다양한 타입스크립트 오류에서 ‘할당 가능한’이라는 문구를 볼 수 있습니다. 이 문구는 집합의 관점에서, ‘~의 원소(값과 타입의 관계)’ 또는 ‘~의 부분 집합(두 타입의 관계)’을 의미합니다.


const a: AB = 'A'; // 정상, 'A'는 집합 {'A', 'B'}의 원소입니다.
const c: AB = 'C'; // 'C' 형식은 'AB' 형식에 할당할 수 없습니다.

‘C’는 유닛 타입입니다. 범위는 단일 값 ‘c’로 구성되며, AB(”A”와 “B”로 이루어진)의 부분 집합이 아니므로 오류입니다. 집합의 관점에서, 타입 체커의 주요 역할은 하나의 집합이 다른 집합의 부분 집합인지 검사하는 것이라고 볼 수 있습니다.

// 정상, {'A', 'B'}는 {'A', 'B'}의 부분 집합입니다.
const ab: AB = Math.random() < 0.5 ? 'A' : 'B';
const ab12: AB12 = ab; // 정상, {'A', 'B'}는 {'A', 'B', 12}의 부분 집합입니다.

declare let twelve: AB12;
const back: AB = twelve;
// 'AB12' 형식은 'AB' 형식에 할당할 수 없습니다.
// '12' 형식은 'AB' 형식에 할당할 수 없습니다.
interface Identified {
    id: string;
}

앞의 인터페이스가 타입 범위 내의 값들에 대한 설명이라고 생각해 보겠습니다. 어떤 객체가 string으로 할당 가능한 id 속성을 가지고 있다면 그 객체는 Identified 입니다.

이처럼 구조적 타이핑 규칙들은 어떠한 값이 다른 속성도 가질 수 있음을 의미합니다. 심지어 함수 호출의 매개변수에서도 다른 속성을 가질 수 있습니다. 이러한 사실은 특정 상황에서만 추가 속성을 허용하지 않는 잉여 속성 체크(excess property checking)만 생각하다 보면 간과하기 쉽습니다.

연산과 관련된 이해를 돕기 위해 값의 집합을 타입이라고 생각해 봅시다.

interface Person {
  name: string;
}
interface Lifespan {
  birth: Date;
  death?: Date;
}
type PersonSpan = Person & Lifespan; // type PersonSpan ≠ never

& 연산자는 두 타입의 인터섹션(intersection, 교집합)을 계산합니다. 언뜻 보기에 PersonLifespan 인터페이스는 공통으로 가지는 속성이 없기 때문에, PersonSpan 타입을 공집합(never 타입)으로 예상하기 쉽습니다.

그러나 타입 연산자는 인터페이스의 속성이 아닌, 값의 집합(타입의 범위)에 적용됩니다. 그리고 추가적인 속성을 가지는 값도 여전히 그 타입에 속합니다. 그래서 PersonLifespan을 둘 다 가지는 값은 인터섹션 타입에 속하게 됩니다.

const ps:PersonSpan = {
    name: "Alan Turning",
    birth: new Date("1912/06/23"),
    death: new Date("1954/06/07"),
};

당연히 앞의 세 가지보다 더 많은 속성을 가지는 값도 PersonSpan 타입에 속합니다. 인터섹션 타입의 값은 각 타입 내의 속성을 모두 포함하는 것이 일반적인 규칙입니다.

📢 규칙이 속성에 대한 인터섹션에 관해서는 맞지만, 두 인터페이스의 유니온에서는 그렇지 않습니다.

type K = keyof (Person | Lifespan); // 타입이 never

앞의 유니온 타입에 속하는 값은 어떠한 키도 없기 때문에, 유니온에 대한 keyof는 공집합(never)이어야만 합니다. 조금 더 명확히 써 보자면 다음과 같습니다.

keyof (A&B) = (keyof A) | (keyof B)
keyof (A|B) = (keyof A) & (keyof B)

이 등식은 타입스크립트의 타입 시스템을 이해하는 데 큰 도움이 될 것입니다.

조금 더 일반적으로 PersonSpan 타입을 선언하는 방법은 extends 키워드를 쓰는 것입니다.

interface Person {
    name: string;
}
interface PersonSpan extends Person {
    birth: Date;
    death?: Date;
}

타입이 집합이라는 관점에서 extends의 의미는 ‘~에 할당 가능한’과 비슷하게, ‘~의 부분 집합’이라는 의미로 받아들일 수 있습니다. PersonSpan 타입의 모든 값은 문자열 name 속성을 가져야 합니다. 그리고 birth 속성을 가져야 제대로 된 부분 집합이 됩니다.

타입이 집합이라는 관점은 배열과 튜플의 관계 역시 명확하게 만듭니다. 예를 들어보면,

const list = [1, 2];
const tuple: [number, number] = list;
// 'number[]' 형식은 '[number, number]' 형식에 할당할 수 없습니다.
//  대상에 2개 요소가 필요하지만, 소스에 더 적게 있을 수 있습니다.

이 코드에서 숫자 배열을 숫자들의 쌍(pair)이라고 할 수는 없습니다. 빈 리스트와 [1]이 그 반례입니다. number[]는 [number, number]의 부분 집합이 아니기 때문에 할당할 수 없습니다.

요약

  1. 타입을 값의 집합으로 생각하면 이해하기 편리하다. 이 집합은 유한(boolean 또는 리터럴 타입)하거나 무한(number 또는 string)합니다.

  2. 타입스크립트 타입은 엄격한 상속 관계가 아니라 겹쳐지는 집합으로 표현된다. 두 타입은 서로 서브타입이 아니면서도 겹쳐질 수 있다.

  3. 한 객체의 추가적인 속성이 타입 선언에 언급되지 않더라도 그 타입에 속할 수 있다.


4️⃣ 코드 작성자의 의도를 드러내려면 어떻게 해야하는지 ❓

그렇다면 “코드 작성자의 의도를 드러낸다” 가 의미하는 것이 무엇일까?

처음 든 생각은 작성자의 의도를 드러내라? 타입스크립트에서 내가 숙지하지 못한 기능이 있나? 코드를 작성할 때, 내가 놓친 부분이 있나? 하고 여러 방면으로 검색도 해보고 교재도 찾아보면서 사용하는 방법이 잘못됐는지, 어디에서 내가 작성한 코드의 의도가 제대로 전달이 안된걸까? 하고 코드도 뒤져보면서 고민을 해봤더랬다.. 🤔

하지만 검색을 하면 할 수록 보이는 것은 타입 정의, 타입 별칭, 제네릭, 인터페이스 등등 내가 알고 있는 사실들에 대해서 얘기하는 것들이었다.

결과적으로 코드 작성자의 의도를 드러낸다 ****라는 말 자체는 특별한 기능이나 코드를 작성하는 방법의 문제가 아니라 타입스크립트를 왜 사용하는지, 어떻게 하면 더 잘 사용할 수 있는지에 대해 고민해보시는 것 과 같은 의미였던 것이였따… 😅

🤔 그러면 나는 과연 타입스크립트를 잘 사용하고 있는걸까 ?

기본적으로 내가 사용한 것들은 함수 인자들의 타입을 지정해주거나 반환 타입을 지정해주는 등 개인 단위로 프로젝트를 진행하고 사용하다 보니 코드 에디터의 자동 완성과 오류 강조를 위한 정도로만 사용하고 있었다.

export type User = {
  email: string;
  password: string;
  birthday: string;
  job: string;
  interests: string[];
};

export type SignupReq = User & { password_confirm: string };
const changePasswordInvalid = (data: PasswordChangeReq) => {
    ...
};

그렇다면 타입스크립트를 통해 나의 의도를 나타내려면 어떻게 코드를 작성하는 것이 좋을까 ?

함수 표현식에 타입 적용하기

자바스크립트(그리고 타입스크립트)에서는 함수 문장(statement)과 함수 표현식(expression)을 다르게 인식합니다.

function rollDice1(sides: number): number { /* ... */ }; // 문장
const rollDice2 = function(sides: number): number { /* ... */ }; // 표현식
const rollDice3 = (sides: number): number => { /* ... */ }; // 표현식

타입스크립트에서는 함수 표현식을 사용하는 것이 좋습니다. 함수의 매개변수부터 반환값까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용할 수 있다는 장점이 있기 때문입니다.

type DiceRollFn = (sides: number) => number;
const rollDice: DiceRollFn = sides => { /* ... */ };

함수 타입의 선언은 불필요한 코드의 반복을 줄입니다. 이전에 작성했던 회원의 정보를 업데이트 하는 로직들에 대한 코드를 다음과 같이 수정할 수 있을 것 같다.

const updateCurrentUser = (updatedUser: User) => {
    if (user && user.email === updatedUser.email) {
      setUser(updatedUser);
      localStorage.setItem("loggedInUser", JSON.stringify(updatedUser));
    }
  };

const updateUserList = (updatedUser: User) => {
  const updatedUsers = users.map((u) =>
    u.email === updatedUser.email ? updatedUser : u
  );
  setUsers(updatedUsers);
  localStorage.setItem("users", JSON.stringify(updatedUsers));
  updateCurrentUser(updatedUser);
};

const login = (userData: User) => {
  localStorage.setItem("loggedInUser", JSON.stringify(userData));
  setUser(userData);
};

세 가지 함수 모두 User 을 인자로 받아 void를 반환하는 함수인데, 이러한 부분을 함수 타입의 선언을 통해 불필요한 코드의 중복을 줄일 수 있을 것 같다.

type UpdateFunction = (updatedUser: User) => void;
const updateCurrentUser: UpdateFunction = (updatedUser) => {
  if (user && user.email === updatedUser.email) {
    setUser(updatedUser);
    localStorage.setItem("loggedInUser", JSON.stringify(updatedUser));
  }
};

이처럼 함수의 매개변수에 타입 선언을 하는 것보다 함수 표현식 전체 타입을 정의하는 것이 코드도 간결하고 안전합니다. 다른 함수의 시그니처와 동일한 타입을 가지는 새 함수를 작성하거나, 동일한 타입 시그니처를 가지는 여러 개의 함수를 작성할 때는 매개변수의 타입과 반환 타입을 반복해서 작성하지 말고 함수 전체의 타입 선언을 적용해야 합니다.

타입 연산과 제너릭 사용으로 반복 줄이기

만약 두 인터페이스가 필드의 부분 집합을 공유한다면, 공통 필드만 골라서 기반 클래스로 분리해 낼 수 있습니다. 이미 존재하는 타입을 확장하는 경우에, 일반적이지는 않지만 인터섹션 연산자(&)를 쓸 수도 있습니다.

type PersonWithBirthDate = Person & { birth: Date };

이런 기법은 유니온 타입(확장할 수 없는)에 속성을 추가하려고 할 때 특히 유용합니다. 이제 다른 측면을 생각해보겠습니다. 만일 애플리케이션의 전체 상태를 표현하는 State 타입과 부분만 표현하는 SideNavState가 있는 경우

interface State {
    userId: string;
    pageTitle: string;
    recentFiles: string[];
    pageContents: string;
}

interface SideNavState {
    userId: string;
    pageTitle: string;
    recentFiles: string[]
}

State를 인덱싱하여 속성의 타입에서 중복을 제거할 수 있습니다.

type SideNavState1 = {
    userId: State['userId'];
    pageTitle: State['pageTitle'];
    recentFiles: State['recentFiles'];
};

그러나 여전히 반복되는 코드가 존재합니다. 이 때 매핑된 타입을 사용하면 좀 더 나아집니다.

type SideNavState2 = {
    [k in 'userId' | 'pageTitle' | 'recentFiles' ]: State[k]
};

매핑된 타입은 배열의 필드를 루프 도는 것과 같은 방식입니다. 이 패턴은 표준 라이브러리에서도 일반적으로 찾을 수 있으며, Pick 이라고 합니다

type Pick<T, K> = { [k in K]: T[k] };

정의가 완벽하지는 않지만 다음과 같이 사용할 수 있습니다.

type SideNavState3 = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;

여기서 Pick은 제너릭 타입입니다. Pick을 사용하는 것은 함수를 호출하는 것과 마찬가지입니다. 마치 함수에서 두 개의 매개변수 값을 받아서 결괏값을 반환하는 것처럼, Pick은 T와 K 두 가지 타입을 받아서 결과 타입을 반환합니다.

요약

  • 타입에 이름을 붙여서 반복을 피해야 합니다. extends를 사용해서 인터페이스 필드의 반복을 피해야 합니다.

  • 타입들 간의 매핑을 위해 타입스크립트가 제공한 도구들을 공부하면 좋습니다. 여기에는 keyof, typeof, 인덱싱, 매핑된 타입들이 포함됩니다.

  • 제너릭 타입은 타입을 위한 함수와 같습니다. 타입을 반복하는 대신 제너릭 타입을 사용하여 타입들 간에 매핑을 하는 것이 좋습니다. 제너릭 타입을 제한하려면 extends를 사용하면 됩니다.

  • 표준 라이브러리에 정의된 Pick, Partial, ReturnType 같은 제너릭 타입에 익숙해져야 합니다.


마무리

지금까지 작성한 내용들 말고도 아직도 내가 모르는 타입스크립트의 대한 내용들이 즐비하다 😂

하지만 지금까지 작성한 내용들만 보더라도 내가 타입스크립트를 사용하면서 얼마나 무지하고 주어진 기능들을 활용하지 못하고 있었는지 절실히 깨달았다.. 🫠

프론트엔드 개발자로서 살아가기 위해 떼어낼 수 없는 타입스크립트 이 녀석.. 앞으로 좀 더 친해지기 위해 더 노력할 생각이다 👀


참고

이펙티브 타입스크립트 : 동작 원리의 이해와 구체적인 조언 62가지

https://academy.dream-coding.com/courses/typescript

https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces