4.Node.js/JavaScript&TypeScript

[TypeScript] 7편. 유틸리티 타입 이해하기: Partial, Readonly, Pick, Omit, 조건부 타입 활용법

쿼드큐브 2025. 12. 24. 08:08
반응형
반응형

 

7편. 유틸리티 타입 이해하기: Partial, Readonly, Pick, Omit, 조건부 타입 활용법

 

📚 목차
1. 기본 유틸리티 타입: Partial<T>와 Readonly<T>
2. 선택 유틸리티 타입 Pick · Omit으로 타입 구조 최적화하기
3. 조건부 타입(Conditional Types)을 활용한 타입 분기 처리
4. 매핑된 타입(Mapped Types)과 실전 사용 패턴

 

유틸리티 타입 삽화 이미지
유틸리티 타입 삽화 이미지

 

📂 [GitHub 예시 코드 보러가기] (https://github.com/cericube/nodejs-tutorials) /TypeScript

 

1. 기본 유틸리티 타입: Partial<T>와 Readonly<T>

- "프로퍼티를 가진 객체 타입” 에 의미 있게 쓰는 유틸리티 -

TypeScript는 자주 사용되는 타입 변환 패턴을 위해 기본 유틸리티 타입(Utility Types)을 제공합니다.

그중에서도 Partial<T>와 Readonly<T>는 객체 타입을 보다 유연하거나 안전하게 다룰 수 있도록 도와주는 대표적인 유틸리티 타입입니다.

유틸리티 타입 설명 예시
Partial<T> 모든 속성을 선택적(Optional)으로 변경 updateUser(id, { name: "홍길동" })
Readonly<T> 모든 속성을 읽기 전용(Readonly)으로 변경 const config: Readonly<Config>

 

🔷 Partial<T> - 모든 속성을 선택적(Optional)으로 만들기

Partial<T>는 타입 T의 모든 속성을 선택적(optional)으로 바꿔주는 유틸리티 타입입니다.

주로 다음과 같은 경우에 유용하게 사용됩니다:
▸ 사용자의 일부 정보만 업데이트할 때
▸ 객체를 점진적으로 초기화할 때

 

✔️ 실무 예제: 사용자 정보 업데이트 함수

// 1. 원본 User 타입 (모든 속성이 필수)
interface User {
  id: number;
  name: string;
  email: string;
}

// 2. Partial을 사용하여 UserUpdate 타입 생성
type UserUpdate = Partial<User>; // 모든 속성이 선택적(optional)이 됨

// 3. 사용 예시
function updateUser(id: number, changes: UserUpdate) {
  console.log(`사용자 ID ${id}의 정보를 업데이트합니다.`);
  // 실제로 데이터베이스 업데이트 로직이 들어갈 수 있습니다
}

// 4. 함수 호출 예시
// name만 전달
updateUser(101, { name: "새로운 이름" });
// id도 수정 가능 (의도적일 수도, 아닐 수도)
updateUser(102, { email: "new@example.com", id: 999 });

Partial<T>은 모든 프로퍼티를 선택적으로 만들기 때문에, 원하지 않는 속성까지 수정될 수 있다는 점에 주의가 필요합니다.

특히 ID처럼 변경하면 안 되는 필드는 별도로 처리하거나 제한을 두는 것이 좋습니다.

 

✔️ 예제: 객체 초기화 시점에서의 활용

interface Todo {
  title: string;
  completed: boolean;
}

function createTodo(data: Partial<Todo>) {
  const defaultTodo: Todo = {
    title: "제목 없음",
    completed: false,
  };

  return { ...defaultTodo, ...data };
}

const t1 = createTodo({ title: "TS 공부" });
const t2 = createTodo({}); // 아무 값 없이도 가능

Partial<T> 덕분에 createTodo 함수는 유연하게 사용할 수 있습니다.

 

🔷 Readonly<T> - 모든 속성을 읽기 전용으로 만들기

Readonly<T>는 타입 T의 모든 속성에 readonly 제어자를 붙여, 값을 변경할 수 없도록 막아주는 유틸리티 타입입니다.
주로 다음 상황에서 사용됩니다:
▸ 설정 객체와 같이 변경되면 안 되는 데이터를 보호할 때
▸ 의도치 않은 데이터 변경을 방지하고자 할 때

 

✔️ 사용 예시: 애플리케이션 설정을 불변으로 유지

// 1. 원본 Config 타입
interface Config {
  apiUrl: string;
  timeout: number;
  debugMode: boolean;
}

// 2. Readonly를 적용
type AppConfig = Readonly<Config>;

const appConfig: AppConfig = {
  apiUrl: "https://api.myapp.com",
  timeout: 5000,
  debugMode: false,
};

// 아래 코드는 컴파일 오류를 발생시킵니다.
// appConfig.timeout = 10000; 
// ❌ 오류: Cannot assign to 'timeout' because it is a read-only property.

이처럼 Readonly<T>를 적용하면, 객체가 변경되지 않도록 타입 수준에서 강제할 수 있어 코드 안정성이 높아집니다.

 

✔️ 예제: 실수 방지용 설정 객체

interface AppSettings {
  theme: "light" | "dark";
  autoSave: boolean;
}

const settings: Readonly<AppSettings> = {
  theme: "light",
  autoSave: true,
};

// settings.theme = "dark"; // ❌ 오류 발생

이 예제처럼 설정 객체가 한 번 정의되면 변경되지 않아야 하는 경우 Readonly<T>는 아주 유용합니다.

 

2. 선택 유틸리티 타입 Pick · Omit으로 타입 구조 최적화하기

- "프로퍼티를 가진 객체 타입” 에 의미 있게 쓰는 유틸리티 -

TypeScript에서는 기존 타입을 변형하거나 일부 속성만 사용하고 싶을 때, 이를 간편하게 도와주는 유틸리티 타입(Utility Types)을 제공합니다.

▸ Pick<T, K>: 원하는 속성만 선택하여 새로운 타입 만들기

▸ Omit<T, K>: 원하지 않는 속성만 제외하고 새로운 타입 만들기

타입 역할 예시
Pick<T, K> 타입 T에서 원하는 속성만 골라 새 타입 생성 공개 사용자 정보, 요약 카드, 미리보기
Omit<T, K> 타입 T에서 지정된 속성만 제거하여 새 타입 생성 POST 요청 바디, 민감 정보 제거 시 등

 

🔷 Pick<T, K>: 원하는 속성만 골라 새 타입 만들기

Pick<T, K>는 타입 T에서 속성 K만 선택적으로 추출하여 새로운 타입을 만듭니다.
▸ T: 원본 타입
▸ K: 가져올 속성 이름들의 유니언 타입 (예: 'name' | 'age')

 

✔️ 실무 예제: 사용자 정보 요약 데이터 만들기

▸ API 응답 중에서 일부 필드만 보여주고 싶을 때 유용합니다.
▸ 사용자의 공개 정보 요약, 간략 카드 UI 등에서 많이 사용됩니다.

interface UserDetails {
  id: number;
  name: string;
  age: number;
  address: string;
  phoneNumber: string;
}

// id와 name만 포함하는 UserSummary 타입을 만듭니다.
type UserSummary = Pick<UserDetails, 'id' | 'name'>;

const summary: UserSummary = {
  id: 5,
  name: "이유나",
  // address: "서울" 🚨 오류: 'address' does not exist on type 'UserSummary'.
};

UserSummary는 id와 name 속성만 포함되므로, 그 외 속성(address 등)을 넣으면 TypeScript가 오류를 발생시켜 줍니다.

 

🔷 Omit<T, K>: 특정 속성을 제외하고 새 타입 만들기

Omit<T, K>는 타입 T에서 K에 해당하는 속성들을 **제외(제거)**한 새로운 타입을 생성합니다.
▸ T: 원본 타입
▸ K: 제외할 속성 이름들의 유니언 타입

 

✔️ 실무 예제: ID를 제외한 사용자 생성용 타입 만들기

interface UserDetails {
  id: number;
  name: string;
  age: number;
  address: string;
  phoneNumber: string;
}

// 서버에 보낼 때는 id가 없으므로 제외
type UserCreatePayload = Omit<UserDetails, 'id'>;

const newUser: UserCreatePayload = {
  name: "최지우",
  age: 25,
  address: "부산",
  phoneNumber: "010-1234-5678"
};

// console.log(newUser.id); // ❌ 오류: 'id'는 존재하지 않습니다.

 

🔷 실전 비교 예제: Pick vs Omit

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

// 상세 페이지: 모든 속성 사용
type ProductDetail = Product;

// 목록 리스트: 요약 정보만 필요
type ProductSummary = Pick<Product, 'id' | 'name' | 'price'>;

// 상품 등록 요청 시: id는 자동 생성되므로 제외
type ProductCreatePayload = Omit<Product, 'id'>;
반응형

 

3. 조건부 타입(Conditional Types)을 활용한 타입 분기 처리

TypeScript에는 값이 아닌 타입에 따라 동작을 분기하는 기능이 존재합니다.

이를 조건부 타입(Conditional Types)이라고 하며, JavaScript의 삼항 연산자와 유사한 문법을 사용합니다:

조건식 ? 참일 때의 타입 : 거짓일 때의 타입

이 기능을 활용하면, 타입 간의 관계를 기반으로 타입을 분기하여 더 유연하고 안전한 타입 로직을 작성할 수 있습니다.

조건부 타입은 다음과 같은 상황에서 매우 유용하게 활용됩니다

상황 활용 예
API 응답 타입 처리 성공 시와 실패 시 다른 구조를 타입으로 표현
값 래핑 여부 처리 특정 타입일 때만 배열로 감싸거나 옵셔널 처리
유틸리티 타입 작성 Exclude, Extract, NonNullable 등도 조건부 타입 기반

 

🔷 기본 문법: T extends U ? X : Y

“만약 타입 T가 타입 U에 할당 가능하면, X 타입을 사용하고 그렇지 않으면 Y 타입을 사용하라.”

type NewType = T extends U ? X : Y;

여기서 extends는 “클래스 상속”이 아닌, “T가 U의 서브타입(subtype)이거나 호환 가능한 타입인지 검사”하는 의미로 사용됩니다.

 

🔷 실전 예제: string이면 배열로 감싸기

아래는 T가 문자열인 경우에는 string[]로 감싸고, 그 외에는 T를 그대로 유지하는 타입을 정의한 예시입니다.

// string이면 string[]로, 아니면 T 그대로 반환
type WrapIfString<T> = T extends string ? string[] : T;
// string이면 string[]를, 아니면 T를 반환하는 타입
type WrapIfString<T> = T extends string ? string[] : T;

// 1. T가 string인 경우
type Result1 = WrapIfString<string>; // ➡️ string[]

// 2. T가 number인 경우 (string에 할당 불가)
type Result2 = WrapIfString<number>; // ➡️ number

// 3. T가 boolean인 경우
type Result3 = WrapIfString<boolean>; // ➡️ boolean

▸ Result1은 string이므로 조건에 해당되어 string[]으로 감싸졌고,

▸ Result2와 Result3은 string이 아니므로 그대로 유지됩니다.

 

🔷 조건부 타입 + 제네릭의 결합: 실전 활용

조건부 타입은 제네릭(Generic)과 함께 사용할 때 매우 강력합니다.

다음은 매개변수의 타입에 따라 반환 타입이 달라지는 함수를 타입 수준에서 표현한 예제입니다.

// 제네릭 타입 T에 대해 조건부 타입을 정의합니다.
// 만약 T가 string이면 반환 타입은 { value: string[] }
// 아니라면 { value: T }가 됩니다.
type Response<T> = T extends string ? { value: string[] } : { value: T };

// wrap 함수는 어떤 타입 T의 값을 받아,
// 그 타입에 따라 다른 구조의 객체를 반환합니다.
function wrap<T>(input: T): Response<T> {
  // 런타임에서 input이 문자열인지 검사합니다.
  if (typeof input === "string") {
    // 문자열이면 배열로 감싸서 반환합니다.
    // 타입스크립트는 조건부 타입을 정확히 추론하지 못할 수 있으므로
    // 명시적으로 as Response<T>로 단언합니다.
    return { value: [input] } as Response<T>;
  } else {
    // 문자열이 아닌 경우는 그대로 value에 담아 반환합니다.
    return { value: input } as Response<T>;
  }
}

// 사용 예시:
const r1 = wrap("hi");
// ⬆️ input이 string이므로 타입은:
//     Response<string> → { value: string[] }
const r2 = wrap(123);
// ⬆️ input이 number이므로 타입은:
//     Response<number> → { value: number }
const r3 = wrap(true);
// ⬆️ input이 boolean이므로 타입은:
//     Response<boolean> → { value: boolean }

▸ wrap("hi")는 문자열이므로 { value: string[] } 타입을 반환하고,

▸ wrap(123)은 숫자이므로 { value: number } 타입을 반환합니다.

 

4. 매핑된 타입(Mapped Types)과 실전 사용 패턴

TypeScript는 기존 타입의 구조를 기반으로 속성들을 변형하여 새로운 타입을 만드는 기능을 제공합니다.
이러한 기능을 매핑된 타입(Mapped Types)이라고 부르며,
JavaScript의 Array.prototype.map()처럼 속성 이름을 기준으로 변형 규칙을 적용하는 방식입니다.

 

🔷 매핑된 타입의 기본 형태

type MyMappedType<T> = {
  [K in keyof T]: NewType;
};

▸ T는 원본 타입입니다.

▸ keyof T는 T의 모든 속성 이름(key)의 집합을 뜻합니다.

▸ K in keyof T는 각 속성 이름에 대해 반복(루프)을 수행합니다.

▸ NewType 자리에 각 속성의 타입을 어떻게 변형할지를 작성합니다.

이러한 구조 덕분에, 타입 수준에서도 루프처럼 동작하는 구조 변형이 가능합니다.

 

✔️ 실무 예제: 함수 타입 속성을 boolean으로 변환

interface TaskList {
  fetchData: () => void;
  processData: () => void;
  renderUI: () => void;
}

// TaskList의 모든 속성을 boolean 타입으로 변환합니다.
type StatusTracker<T> = {
  [K in keyof T]: boolean;
};

type TaskStatus = StatusTracker<TaskList>;
/*
TaskStatus는 다음과 같습니다:
{
  fetchData: boolean;
  processData: boolean;
  renderUI: boolean;
}
*/

const currentStatus: TaskStatus = {
  fetchData: true,
  processData: false,
  renderUI: false,
};

 

🔷 매핑된 타입 기반 유틸리티 타입의 구현 원리

Readonly<T>, Partial<T>, Required<T>와 같은 유틸리티 타입도 모두 매핑된 타입을 이용해 구현되어 있습니다.

// 모든 속성을 readonly로 변환
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

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

type ReadonlyPerson = MyReadonly<Person>;
/*
ReadonlyPerson은 다음과 같습니다:
{
  readonly name: string;
  readonly age: number;
}
*/

//이제 ReadonlyPerson 타입은 각 속성이 readonly가 되었기 때문에 값을 변경할 수 없습니다.
const user: ReadonlyPerson = { name: "Jane", age: 30 };
// user.age = 31; // ❌ 오류 발생: Cannot assign to 'age' because it is a read-only property.

 

🔷 조건부 타입 기반 유틸리티 타입: 실무에서 자주 쓰는 타입 변형 도구들

조건부 타입(Conditional Types)은 특정 타입이 어떤 조건을 만족하는지에 따라 타입을 분기할 수 있는 고급 기능입니다.
이러한 조건부 타입을 활용해 TypeScript는 다양한 공식 유틸리티 타입(utility types)을 미리 제공하고 있으며,

이들은 실제 실무 프로젝트에서 타입 안정성과 코드 간결성을 동시에 향상시키는 데 매우 유용합니다.

유틸리티 타입 요약
NonNullable<T> null, undefined 제거
Exclude<T, U> 유니언에서 특정 타입 빼기
Extract<T, U> 유니언에서 특정 타입망 추출
ReturnType<T> 함수의 반환 타입 추출
Parameters<T> 함수의 매개변수 타입 추출 (튜플)
// ✅ 실무에서 자주 쓰이는 조건부 타입 기반 유틸리티 타입 예제 모음

// 1. NonNullable<T>
// => 타입에서 null과 undefined를 제거한 타입을 생성합니다.
type WithNull = string | null | undefined;
type WithoutNull = NonNullable<WithNull>; // 결과: string

const name: WithoutNull = "홍길동"; // ✅ 정상
// const name2: WithoutNull = null; // ❌ 오류 발생
// const name3: WithoutNull = undefined; // ❌ 오류 발생


// 2. Exclude<T, U>
// => 타입 T에서 U를 제거한 타입을 생성합니다.
type Role = "admin" | "user" | "guest";
type VisibleRole = Exclude<Role, "admin">; // 결과: "user" | "guest"

const role1: VisibleRole = "user";   // ✅
const role2: VisibleRole = "guest";  // ✅
// const role3: VisibleRole = "admin"; // ❌ 오류 발생


// 3. Extract<T, U>
// => 타입 T에서 U에 해당하는 부분만 추출합니다.
type Status = "success" | "error" | "loading";
type FinalStatus = Extract<Status, "success" | "error">; // 결과: "success" | "error"

const status1: FinalStatus = "success"; // ✅
// const status2: FinalStatus = "loading"; // ❌ 오류 발생


// 4. ReturnType<T>
// => 함수의 반환 타입을 추출합니다.
function getUser() {
  return {
    id: 1,
    name: "Jane",
    isAdmin: false,
  };
}

type User = ReturnType<typeof getUser>; // { id: number; name: string; isAdmin: boolean }

const u: User = {
  id: 1,
  name: "Jane",
  isAdmin: true,
}; // ✅


// 5. Parameters<T>
// => 함수의 매개변수 타입을 튜플로 추출합니다.
function sendMessage(to: string, body: string, urgent?: boolean) {
  console.log(`[Message] To: ${to}, Body: ${body}, Urgent: ${urgent}`);
}

// [string, string, (boolean | undefined)?]
type SendMessageParams = Parameters<typeof sendMessage>; 

const args: SendMessageParams = ["alice@example.com", "Hello!", true];
sendMessage(...args); // ✅ 정상 실행

 

 

 


※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.

반응형

 

반응형