고급 TypeScript 기술: Conditional Types, Mapped Types, 그리고 Type Inference 전략으로 더 나은 코드

발행: (2026년 1월 6일 오후 09:08 GMT+9)
10 min read
원문: Dev.to

Source: Dev.to

베스트셀러 작가로서, 제 책들을 Amazon에서 살펴보시길 초대합니다 Amazon. 제 Medium을 팔로우하는 것을 잊지 마세요 Medium 그리고 응원해 주세요. 감사합니다! 여러분의 응원은 큰 힘이 됩니다!

TypeScript의 타입 시스템 – 슈퍼 파워

TypeScript의 타입 시스템은 점차 익혀 나갈 수 있는 슈퍼 파워와도 같습니다. 저는 string, number, 기본 인터페이스 같은 간단한 어노테이션부터 시작했고, 시간이 지나면서 코드의 형태와 동작을 놀라울 정도로 정밀하게 기술할 수 있는 또 다른 레이어의 기법들을 발견했습니다. 이러한 방법들은 코드가 실행되기 전에 실수를 잡아주고, 에디터가 똑똑하게 느껴지게 합니다. 아래에서는 가장 강력한 방법들을 기본부터 고급까지 순서대로 살펴보겠습니다.

1. Conditional Types

타입을 위한 “if 문”이라고 생각하면 됩니다. 다른 타입에 따라 어떤 타입이 될지를 결정하게 해 주어, 유연한 유틸리티를 만들 때 유용합니다.

type IsNumber<T> = T extends number ? true : false;

type TestA = IsNumber<string>; // false
type TestB = IsNumber<42>;    // true

infer inside a conditional type

다른 타입의 일부분을 캡처합니다. 대표적인 예는 아직 모르는 함수의 반환 타입을 추출하는 것입니다.

type WhatDoesThisReturn<T> =
  T extends (...args: any) => infer ReturnType ? ReturnType : never;

function fetchData() {
  return { id: 1, title: 'Item' };
}

type FetchedData = WhatDoesThisReturn<typeof fetchData>;
// FetchedData는 이제 { id: number; title: string; } 입니다.

Distribution over unions

Conditional types는 자동으로 유니언 멤버마다 분배됩니다.

type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>; // string[] | number[]

Preventing distribution

체크를 튜플로 감싸면 분배를 멈출 수 있습니다.

type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNoDistribute<string | number>; // (string | number)[]

2. Mapped Types

기존 타입의 각 프로퍼티를 변형해서 새로운 타입을 만듭니다. Array.map()이 배열 요소를 변형하듯, 객체 키를 변형합니다.

Basic mapped types

type MakeAllOptional<T> = {
  [Key in keyof T]?: T[Key];
};

type MakeAllReadonly<T> = {
  readonly [Key in keyof T]: T[Key];
};

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

type PartialUser = MakeAllOptional<User>;
// { id?: number; name?: string; }

type ReadonlyUser = MakeAllReadonly<User>;
// { readonly id: number; readonly name: string; }

Adding / removing modifiers

?(optional)이나 readonly를 토글하려면 + 또는 -를 사용합니다.

type MakeRequired<T> = {
  [Key in keyof T]-?: T[Key];
};

type UserWithOptionalId = { id?: number; name: string };
type RequiredUser = MakeRequired<UserWithOptionalId>;
// { id: number; name: string; }

Key remapping with as

// 함수인 프로퍼티만 남깁니다
type FunctionProperties<T> = {
  [Key in keyof T as T[Key] extends Function ? Key : never]: T[Key];
};

// 모든 키에 'get' 접두사를 붙입니다
type Getters<T> = {
  [Key in keyof T as `get${Capitalize<string & Key>}`]: () => T[Key];
};

interface DataModel {
  value: number;
  calculate(): number;
}

type JustFunctions = FunctionProperties<DataModel>; // { calculate: () => number; }
type DataGetters = Getters<DataModel>;
// { getValue: () => number; getCalculate: () => () => number; }

3. Template Literal Types

템플릿 문자열 문법을 이용해 문자열 리터럴 타입을 조작합니다. 타입‑안전한 라우트, CSS 클래스 이름, 메서드 이름 등을 정의할 때 유용합니다.

type Color = 'red' | 'blue';
type Size = 'sm' | 'lg';

type ButtonClass = `btn-${Size}-${Color}`;
// "btn-sm-red" | "btn-sm-blue" | "btn-lg-red" | "btn-lg-blue"

const myClass: ButtonClass = 'btn-lg-red'; // ✅ 정상
// const badClass: ButtonClass = 'btn-md-green'; // ❌ 오류

Parsing strings with infer

type ExtractParts<T> = T extends `btn-${infer S}-${infer C}` ? [S, C] : never;
type Parts = ExtractParts<'btn-sm-red'>; // ["sm", "red"]

4. Inference Operators

typeof – 값의 정적 타입을 얻습니다

const config = { host: 'localhost', port: 3000 };
type Config = typeof config;
// { host: string; port: number; }

keyof – 객체의 키를 유니언으로 만듭니다

(이하 내용은 다음 파트에서 이어집니다)

interface Product {
  id: number;
  name: string;
  price: number;
}
type ProductKey = keyof Product; // "id" | "name" | "price"

인덱스 접근 타입 – 속성 타입 조회

type ProductNameType = Product['name'];   // string
type ProductPriceType = Product['price']; // number

이러한 기술—조건부 타입, 매핑된 타입, 템플릿 리터럴 타입, 그리고 추론 연산자—은 더 안전하고 표현력이 풍부한 TypeScript를 작성할 수 있게 해주는 강력한 도구 상자를 제공합니다. 이를 실험하고 결합해 보세요. 그러면 에디터가 진정한 코딩 파트너가 되는 것을 체감할 수 있을 겁니다. 즐거운 코딩 되세요!

Source:

인덱스 접근 타입, 유틸리티 타입, 그리고 제네릭 in TypeScript

아래는 인덱스 접근 타입, 유틸리티 타입, 제네릭 제약, 그리고 재귀 타입을 사용하는 간결한 예시들입니다.

인덱스 접근 타입

interface Product {
  id: number;
  name: string;
  price: number;
}

// Get the type of a single property
type ProductId = Product['id']; // number

// Union of several properties
type IdOrName = Product['id' | 'name']; // number | string

깊은 인덱스 접근

interface APIResponse {
  data: {
    user: {
      id: string;
      profile: { name: string };
    };
  };
}

// Extract a deeply‑nested property type
type UserProfileName = APIResponse['data']['user']['profile']['name']; // string

유틸리티 타입: Pick & Omit

interface FullUser {
  id: number;
  name: string;
  email: string;
  passwordHash: string;
  createdAt: Date;
}

/* Public‑facing data */
type PublicUser = Pick<FullUser, 'id' | 'name' | 'email'>;
/* { id: number; name: string; email: string; } */

/* Remove sensitive fields */
type SafeUser = Omit<FullUser, 'passwordHash'>;
/* { id: number; name: string; email: string; createdAt: Date; } */

Pick 작동 방식 (단순화)

// A minimal re‑implementation of Pick
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

type PickedName = MyPick<FullUser, 'name' | 'email'>;
// { name: string; email: string; }

제네릭 타입 매개변수 제약하기

// Function that works only with objects that have an `id` property
function getById<T extends { id: string }>(items: T[], id: string): T | undefined {
  return items.find(item => item.id === id);
}

const users = [{ id: 'a1', name: 'Alice' }, { id: 'b2', name: 'Bob' }];
const found = getById(users, 'b2'); // { id: string; name: string; }

하나의 제네릭을 다른 제네릭으로 제약하기

// Merge two objects; the result type is the intersection of both inputs
function merge<A, B>(a: A, b: B): A & B {
  return { ...a, ...b };
}

const result = merge({ foo: 1 }, { bar: 'two' });
// result: { foo: number; } & { bar: string; }

재귀 타입

트리 구조

interface TreeNode<T> {
  value: T;
  children?: TreeNode<T>[];
}

const tree: TreeNode<string> = {
  value: 'root',
  children: [
    { value: 'child1' },
    { value: 'child2', children: [{ value: 'grandchild' }] }
  ]
};

깊게 선택적(DeepPartial)

type DeepPartial<T> = T extends object
  ? { [P in keyof T]?: DeepPartial<T[P]> }
  : T;

interface Settings {
  user: {
    theme: string;
    dashboard: { widgets: string[] };
  };
}

type PartialSettings = DeepPartial<Settings>;
/* All properties (user, theme, dashboard, widgets) become optional at any depth */

정리

typeofkeyof를 사용해 중복을 없애고, 매핑 혹은 조건부 타입으로 작은 유틸리티 타입을 만들어 특정 문제(예: 인터페이스에서 모든 문자열 속성 추출)를 해결하세요. 시간이 지나면 이러한 기법이 일상적인 워크플로우의 일부가 되어, TypeScript 컴파일러를 진정한 파트너로 만들어 계약을 강제하고 버그를 줄여줍니다.

📘 최신 전자책 확인하기

Watch the free preview on YouTube

내용이 마음에 드신다면 좋아요, 공유, 댓글, 구독 부탁드립니다!

101 Books

101 Books는 저자 Aarav Joshi가 공동 설립한 AI 기반 출판사입니다. 첨단 AI 기술을 활용해 출판 비용을 낮추고—일부 책은 $4에 판매됩니다—양질의 지식을 모두에게 제공하고자 합니다.

  • Golang Clean Code – Amazon에서 구매 가능
  • Aarav Joshi를 검색하면 더 많은 책을 찾을 수 있으며 특별 할인도 받을 수 있습니다!

우리의 작품

프로젝트를 살펴보세요:

우리는 Medium에 있습니다

Tech Koala Insights – 더 많은 기사, 튜토리얼, 그리고 현대 개발 주제에 대한 심층 탐구를 위해 팔로우하세요.

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Back to Blog

관련 글

더 보기 »

2026년에 배우고 싶은 3가지

n8n은 Dev 유튜버들에 의해 몇 차례 다뤄졌으며 제 관심을 끌었습니다. 이것은 오픈소스 워크플로 자동화 도구로, fair‑code licensed이며, 강력…