TypeScript Mapped Type 객체 타입을 기반으로 새 타입 만들기

··설명 약 8분·작성: 해비·블로그 소개

TypeScript Mapped Type으로 기존 객체에서 새 타입을 어떻게 만드나요?

Mapped Type은 기존 객체의 키를 순회해 같은 규칙의 새 타입을 만듭니다. [K in keyof T]에 값 타입과 optional·readonly 조건을 붙이면 필드별 오류 메시지나 변경 상태를 중복 선언하지 않아도 됩니다.

본문 근거: TypeScript Mapped Types·modifier

직접 확인: 오류에서 수정까지

TypeScript Mapped Type 객체 타입을 기반으로 새 타입 만들기 · 독립 실습

얻을 결과: [K in keyof Task]?: string으로 원본 키와 오류 메시지를 연결합니다.

예제 크게 보기 (새 탭)

단독 검사는 tsconfig.json이 없는 새 폴더에서 example.ts 한 파일씩 교체하며 진행합니다. TypeScript가 없다면 설치 글에서 Node.js/npm·TypeScript 준비를 확인하세요. 타입 검사 성공은 코드 실행 성공과 구분합니다.

독자 질문: Task의 모든 필드에 편집 오류 상태를 붙이려면?

얻을 결과: [K in keyof Task]?: string으로 원본 키와 오류 메시지를 연결합니다.

오류 코드를 별도 example.ts에 복사합니다.

type Task = { id: string; title: string };
type Errors = { [K in keyof Task]?: string };
const errors: Errors = { title: 123 };

예상 타입 오류 (TS2322): title 오류 값의 number는 string에 할당할 수 없습니다. 메시지를 문자열로 바꿉니다.

위 코드를 다음 수정 예제로 교체합니다.

type Task = { id: string; title: string };
type Errors = { [K in keyof Task]?: string };
const errors: Errors = { title: '제목을 입력하세요' };
console.log(errors.title);
npx tsc --strict --noEmit example.ts

기존 최소 예제 확인 기록: TypeScript 7.0.2의 별도 최소 예제에서 오류 TS2322, 수정 후 exit 0 확인. TaskFlow 전체 체크포인트·브라우저 실행은 미확인.

다음 학습: 14회차에서 Conditional Type과 infer로 결과 타입을 추출합니다.

기본 문법

아래 개념 조각에서 참조하는 학습용 타입입니다. 프로젝트 파일을 import하지 않습니다. 같은 이름을 다시 선언하는 비교 코드는 별도 파일에서 확인하세요.

type Task = { id: string; title: string; completed: boolean };
type TaskFilter = 'all' | 'active' | 'completed';
type Flags<T> = {
  [K in keyof T]:
    boolean;
};

Task를 넣으면:

type TaskFlags =
  Flags<Task>;

결과:

type TaskFlags = {
  id: boolean;
  title: boolean;
  completed: boolean;
};

모든 Task key를 그대로 유지하면서 value 타입만 boolean으로 바꿉니다.

optional modifier

type Optional<T> = {
  [K in keyof T]?:
    T[K];
};

Partial<T>와 비슷한 역할입니다.

optional을 제거할 수도 있습니다.

type RequiredFields<T> = {
  [K in keyof T]-?:
    T[K];
};

readonly modifier

type ReadonlyFields<T> = {
  readonly
  [K in keyof T]:
    T[K];
};

readonly 제거:

type Mutable<T> = {
  -readonly
  [K in keyof T]:
    T[K];
};

필드별 오류 타입

폼의 각 property마다 오류 문자열이 있을 수 있습니다.

type FieldErrors<T> = {
  [K in keyof T]?:
    string;
};
type TaskForm = {
  title: string;
  completed: boolean;
};


type TaskErrors =
  FieldErrors<TaskForm>;

결과:

{
  title?: string;
  completed?: string;
}

TaskForm property가 바뀌면 오류 타입도 자동으로 따라갑니다.

Record와 차이

Record는 key union과 value 타입으로 map을 만드는 간단한 utility입니다.

type StatusMap =
  Record<
    TaskFilter,
    string
  >;

Mapped Type은 기존 타입의 modifier와 property type을 참고해 더 복잡한 변환을 만들 때 유용합니다.

key remapping 예고

Mapped Type에서는 as를 사용해 key 이름을 바꾸는 고급 패턴도 가능합니다.

type Getters<T> = {
  [K in keyof T
    as `get${Capitalize<
      string & K
    >}`]:
      () => T[K];
};

학습 과정에서는 타입 트릭보다 기본 변환 원리를 우선합니다.

언제 custom Mapped Type을 만들까

  • 같은 변환 규칙이 반복될 때
  • 기존 타입과 새 타입의 key 관계를 유지해야 할 때
  • Utility Type만으로 의도를 표현하기 어려울 때

한 번만 사용하는 복잡한 타입이라면 명시적으로 직접 작성하는 것이 더 읽기 쉬울 수 있습니다.

Mapped Type의 반복은 타입 선언에서만 일어납니다. FieldDirty 타입을 만들었다고 실행 중 객체에 모든 키가 자동 생성되지는 않으므로 실제 초기 상태 값도 별도로 작성해야 합니다.

원리 확인: TypeScript Mapped Types·modifier (2026-10-12). 아래 기대 결과와 기존 실행 기록은 구분해서 읽습니다.

학습 완료 체크리스트

원본 폼에 새 필드를 추가했을 때 errors와 dirty에 무엇이 달라져야 할지 설명해 보세요. 선택 오류 맵은 해당 메시지를 생략할 수 있지만 필수 dirty 맵은 새 키의 boolean을 요구해야 합니다.


작은 변형으로 확인하기

Errors에서 ?를 제거하면 id와 title 오류 문자열이 모두 필요합니다. 원래 선택적 오류 맵과 필수 dirty 맵의 차이를 확인하세요.

선택 실습: 할 일 앱에 적용하기

TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 JavaScript 코드를 TypeScript로 확장합니다.

Task 편집 폼의 field별 dirty/error 상태를 TaskForm 타입에서 파생합니다.

선택: 연재 프로젝트와 전체 자료

연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.

TaskFlow 프로젝트 적용

TypeScript Mapped Type 객체 타입을 기반으로 새 타입 만들기 · 단계별 TaskFlow

타입을 적용한 TaskFlow에서 작업을 추가·완료 전환·삭제하세요. 타입만 바뀐 단계는 이전과 화면이 같습니다.

예제 크게 보기 (새 탭)

시작·완료 파일과 적용 순서에서 이번 변경 파일의 전체 내용과 적용 순서를 확인합니다.

시작본과 이번 변경

TypeScript Utility Types Partial Pick Omit Record 실전의 완료 체크포인트를 기준으로 이어갑니다. 변경하지 않는 파일은 그 시작 상태를 유지합니다.

src/types/form.ts에 FieldErrors와 FieldDirty를 추가하고 편집 입력 TaskForm에서 파생합니다. 사용하는 파일은 두 타입을 import해야 합니다. TaskForm에 필드를 더했을 때 FieldDirty에는 해당 boolean이 필수가 되는지 검사합니다.

이전 체크포인트

typescript-step-12-utility-types

이번 목표

Task 편집 폼의 field별 dirty/error 상태를 TaskForm 타입에서 파생합니다.

전체 파일·코드 보기

src/types/form.ts

export type FieldErrors<T> = {
  [K in keyof T]?:
    string;
};


export type FieldDirty<T> = {
  [K in keyof T]:
    boolean;
};

Task 편집 입력

type TaskForm = {
  title: string;
  completed: boolean;
};


type TaskFormErrors =
  FieldErrors<TaskForm>;


type TaskFormDirty =
  FieldDirty<TaskForm>;

TaskForm에 property가 추가되면 dirty/error map도 같은 key 구조를 유지합니다.

검증

npx tsc --noEmit

중간부터 시작하는 전체 파일과 적용 순서

이번 글 시작 ZIP · 이번 글 완료 ZIP

시작 ZIP은 직전 프로젝트 단계 typescript-step-12-utility-types의 실제 완료 파일입니다. 프로젝트 순서를 이어가는 기준이며 사이트의 기술 읽기 순서와는 구분합니다.

  1. 시작 ZIP을 새 폴더에 풉니다. 이 폴더가 작업 루트이며 기존 체크포인트 파일을 별도로 찾을 필요가 없습니다.
  2. FieldErrors·FieldDirty와 TaskForm 파생 타입을 하나의 전체 파일로 제공합니다.
  3. 아래 변경 파일을 같은 상대 경로에 추가하거나 전체 교체합니다. 표시하지 않은 파일과 images는 시작본 그대로 유지합니다. package-lock.json은 완료 ZIP에 있는 파일을 함께 적용합니다.
  4. 작업 루트에서 명령을 실행하고 예상 결과를 확인합니다. 완료 ZIP은 직접 적용한 결과를 비교하는 기준입니다.

추가·교체 파일

  • CHANGES.md — 전체 교체
  • README.md — 전체 교체
  • tests/contracts.ts — 전체 교체
  • src/types/form.ts — 추가

실행과 확인

npm ci
npm run dev
npm run typecheck
npm test
npm run build

개발 서버는 실행 상태로 두고 다른 터미널에서 검사 명령을 실행하세요. Node.js 24.18.0에서 검증했습니다. JavaScript는 http://127.0.0.1:4173, TypeScript는 Vite 실행 출력의 주소로 접속합니다.

예상 결과: errors는 선택 필드, dirty는 모든 필드를 요구하므로 completed 누락이 거부됩니다.

기존 ZIP 검증 기록: TypeScript strict 검사·긍정/부정 타입 계약·런타임 검사·Vite 빌드 통과. 시작 ZIP에 이 글의 변경 파일만 적용한 결과와 완료 ZIP의 모든 파일 내용이 일치했습니다. 브라우저 화면 배치·실제 기기·외부 CDN 가용성은 이 검사에 포함하지 않았습니다.

이 절의 전체 파일은 ZIP과 일치합니다. 위쪽 원문의 짧은 예제나 생략 표시가 있는 부분 코드는 개념·변경 위치 설명용입니다. 서버 저장과 인증은 제공하지 않으며 JSON fixture는 로컬 읽기 실습용입니다.

복사할 변경 파일 전체 코드

tests/contracts.ts — 전체 파일

import type { Task } from '../src/types/task.js';
const valid: Task = {id:'1',title:'공부',completed:false};
// @ts-expect-error completed must be boolean
const invalid: Task = {id:'1',title:'공부',completed:'no'};
void valid; void invalid;
import { readTitle } from '../src/core/selection.js';
const nullable = readTitle(null);
// @ts-expect-error null result must be narrowed
const definite: string = nullable;
void definite;
import type { TaskKey } from '../src/types/task-key.js';
// @ts-expect-error nonexistent property
const badKey: TaskKey = 'missing';
void badKey;
import { TASK_FILTER_LABELS } from '../src/constants/task.js';
// @ts-expect-error missing is not a filter key
const badLabel = TASK_FILTER_LABELS.missing;
void badLabel;
import { getValue } from '../src/core/collection.js';
// @ts-expect-error nonexistent key must fail
getValue({title:'ok'}, 'missing');
import type { TaskFormDirty } from '../src/types/form.js';
// @ts-expect-error completed is required by mapped type
const dirty: TaskFormDirty = {title:true};
void dirty;

src/types/form.ts — 전체 파일

export type FieldErrors<T> = {
  [K in keyof T]?:
    string;
};


export type FieldDirty<T> = {
  [K in keyof T]:
    boolean;
};
export type TaskForm = {
  title: string;
  completed: boolean;
};


export type TaskFormErrors =
  FieldErrors<TaskForm>;


export type TaskFormDirty =
  FieldDirty<TaskForm>;

완료 체크포인트

typescript-step-13-mapped-types

다음 학습

다음 글에서는 타입 조건에 따라 결과 타입을 선택하는 Conditional Type과 infer를 배웁니다.

다음 글: TypeScript Conditional Type과 infer 조건에 따라 타입 만들기

글 소개·학습 준비·전체 목차
TypeScript Mapped Type 객체 타입을 기반으로 새 타입 만들기

TypeScript · 13 / 22 · 필수 학습 · Mapped Type

객체의 필드별 오류·변경 상태 타입을 중복 없이 만들려면? Mapped Type의 [K in keyof T]로 원본 키를 순회하고 각 속성에 새 타입과 optional·readonly 규칙을 적용합니다. 필드 오류는 선택적으로, dirty 상태는 필요한 키를 모두 갖도록 설계하는 차이를 확인합니다.

Partial, Pick 같은 Utility Type도 내부적으로 이런 타입 변환 개념과 연결됩니다.

이번 글에서 배우는 내용

  • keyof 반복
  • [K in keyof T]
  • optional modifier
  • readonly modifier
  • property별 상태 map
  • Utility Type과의 관계

단독 학습 선수 지식

keyof·제네릭·Partial과 Record을 알면 됩니다. 이전 TaskFlow 파일 없이 직접 확인 예제로 학습할 수 있습니다.

목차

이 글이 도움이 되었나요?

조회 중

읽음 기록

읽음 — / 전체 22개 · 필수 21개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기