<Demopeu/>

타입 조작(type manipulation)

타입 조작(type manipulation)

referencetypescriptindexed-access-typekeyofmapped-typestemplate-literal

📌 인덱스드 액세스 타입(Indexed Access Types)

인덱스를 이용해 특정 프로퍼티를 추출하는 타입이다.

interface Post {
  title: string;
  content: string;
  author: {
    id: number;
    name: string;
  };
}

const post: Post = {
  title: "게시글 제목",
  content: "게시글 본문",
  author: {
    id: 1,
    name: "김동현",
  },
};

function printAuthorInfo(author: { id: number; name: string }) {
  console.log(`${author.id} - ${author.name}`);
}

이렇게 작성하면 Post 타입의 author 프로퍼티가 수정될 때마다 해당 타입도 같이 수정해줘야 한다. 따라서,

function printAuthorInfo(author: Post['author']) {
  console.log(`${author.id} - ${author.name}`);
}

이때 중요한 건 [] 안에는 반드시 타입이 들어가야 한다는 점이다. 그래서 다음과 같은 문법은 사용할 수 없다.

const key = 'author';

function printAuthorInfo(author: Post[key]) {  // ❌ Error: 타입만 명시할 수 있다.
  console.log(`${author.id} - ${author.name}`);
}

또한, 배열에서도 타입을 추출할 수 있다.

type PostList = {
  title: string;
  content: string;
  author: {
    id: number;
    name: string;
    age: number;
  };
}[];

function printAuthorInfo(author: PostList[number]['author']) {
  console.log(`${author.id} - ${author.name}`);
}

튜플은 크기가 고정되어 있기 때문에, 아래와 같이 사용할 수 있다.

type Tup = [number, string, boolean];

type Tup0 = Tup[0];
// number

type Tup1 = Tup[1];
// string

type Tup2 = Tup[2];
// boolean

type Tup3 = Tup[number];
// number | string | boolean

한 가지 주의할 점은, 튜플 타입에 인덱스드 액세스 타입을 사용할 때 인덱스에 number 타입을 넣으면 마치 튜플을 일반 배열처럼 인식해서 모든 요소의 타입을 합친 유니언 타입을 추출하게 된다는 점이다.


📌 keyof 연산자

객체 타입으로부터 프로퍼티의 모든 key들을 string literal union type으로 추출하는 연산자이다.

keyof 연산자는 오직 "타입"에만 적용 가능한 연산자다.

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

function getProperty(obj: Person, key: 'name' | 'age') {
  return obj[key];
}

이런 식으로 타입을 계속 늘릴 수는 없으므로, 이럴 때 사용하는 것이 keyof 연산자다.

function getProperty(obj: Person, key: keyof Person) {
  return obj[key];
}

typeof 연산자와 같이 사용하기

  function getProperty(obj: Person, key: keyof typeof obj) {
    return obj[key];
  }

이런식의 타입 추론도 가능하다.

📌 맵드 타입(Mapped Types)

중복을 피하기 위해 다른 타입을 바탕으로 새로운 타입을 생성할 수 있는 마법 같은 문법

interface User {
  id: number;
  name: string;
  age: number;
}

interface PartialUser {
  id?: number;
  name?: number;
  age?: number;
}

interface BooleanUser {
  id?: boolean;
  name?: boolean;
  age?: boolean;
}

이런 식으로 비슷하지만 조금씩 다른 타입을 필요에 따라 계속 정의해야 할 때가 있다.
이럴 때 필요한 것이 **맵드 타입(Mapped Types)**이며, type 별칭 문법으로만 사용할 수 있다.

type PartialUser = {
  [key in 'id' | 'name' | 'age']?: User[key];
};

type BooleanUser = {
  [key in 'id' | 'name' | 'age']?: boolean;
};

이렇게 간편하게 줄일 수 있다.
하지만 위에서 본 keyof 연산자를 함께 사용하면 더 안전하고 간편하게 정의할 수 있다.

type PartialUser = {
  [key in keyof User]?: User[key];
};

type BooleanUser = {
  [key in keyof User]?: boolean;
};

Mapping Modifiers

매핑 중에는 추가로 readonly?를 사용할 수 있다. 또한 - 또는 +를 접두사로 붙여 이런 수정자를 추가하거나 제거할 수 있다.

  type CreateMutable<Type> = {
    -readonly [Property in keyof Type]: Type[Property];
  };

  type LockedAccount = {
    readonly id: string;
    readonly name: string;
  };

  type UnlockedAccount = CreateMutable<LockedAccount>;

// UnlockedAccount는 다음과 같은 타입이 된다.
// {
//   id: string;
//   name: string;
// }

그 외에도 as를 사용해 매핑된 키 이름을 다시 한 번 변환할 수 있고, never를 사용해 특정 프로퍼티를 제외하는 것도 가능하다.


📌 템플릿 리터럴 타입(Template Literal Types)

type Color = 'red' | 'blue' | 'green';
type Animal = 'dog' | 'cat' | 'mouse';

type ColorAnimal = `${Color}-${Animal}`;

이런 식으로 문자열 리터럴 유니언 타입들을 조합해서 새로운 문자열 리터럴 타입을 만들 수 있다.