TypeScript 입문 9편 - 유틸리티 타입 활용

(수정: ) learning by Seven Fingers Studio 18분
TypeScript유틸리티타입PartialPickOmit웹개발

typescript guide 09 utility types

업데이트 함수 만들 때 모든 필드 선택적으로 받고 싶은데 타입을 또 정의해야 하나 고민했어요. 그때 Partial 알게 됐는데 완전 편하더라고요. 이런 유틸리티 타입들을 제공해주니 개발 생산성이 확 올라갑니다.

Partial - 모든 속성을 선택적으로

모든 속성을 optional(?)로 만들어줘요.

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

// 모든 속성이 선택적이 됨
type PartialUser = Partial<User>;

const updateUser: PartialUser = {
  name: "김철수"  // id, email, age 없어도 OK
};

console.log(updateUser);

실행 결과:

✓ 컴파일 성공
{ name: '김철수' }

필요한 속성만 전달할 수 있습니다

실전 활용: 업데이트 함수

function updateUserInfo(id: number, updates: Partial<User>): void {
  console.log(`사용자 ${id} 업데이트:`, updates);
}

updateUserInfo(1, { name: "이영희" });
updateUserInfo(2, { email: "new@example.com", age: 30 });

실행 결과:

✓ 컴파일 성공
사용자 1 업데이트: { name: '이영희' }
사용자 2 업데이트: { email: 'new@example.com', age: 30 }

업데이트할 필드만 선택적으로 전달할 수 있어 유용합니다

Required - 모든 속성을 필수로

Partial의 반대예요. 선택적 속성을 필수로 만들어줘요.

interface Config {
  apiUrl?: string;
  timeout?: number;
  retryCount?: number;
}

// 모든 속성이 필수가 됨
type RequiredConfig = Required<Config>;

const config: RequiredConfig = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  retryCount: 3
  // 하나라도 빠지면 에러!
};

console.log(config);

실행 결과:

✓ 컴파일 성공
{ apiUrl: 'https://api.example.com', timeout: 5000, retryCount: 3 }

모든 속성을 반드시 제공해야 합니다

Readonly - 모든 속성을 읽기 전용으로

수정 불가능한 객체를 만들어요.

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

const user: Readonly<User> = {
  id: 1,
  name: "김철수"
};

console.log(user);
// user.name = "이영희";  // 에러! 읽기 전용

실행 결과:

✗ 타입 에러
Cannot assign to 'name' because it is a read-only property

Readonly로 감싸면 모든 속성이 읽기 전용이 됩니다

Pick<T, K> - 특정 속성만 선택

필요한 속성만 골라내요.

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

// name과 email만 선택
type UserPreview = Pick<User, "name" | "email">;

const preview: UserPreview = {
  name: "김철수",
  email: "kim@example.com"
};

console.log(preview);

실행 결과:

✓ 컴파일 성공
{ name: '김철수', email: 'kim@example.com' }

선택한 속성만 포함하는 새 타입이 생성됩니다

Omit<T, K> - 특정 속성만 제외

Pick의 반대예요. 특정 속성을 빼고 나머지를 가져와요.

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

// password만 제외
type UserResponse = Omit<User, "password">;

const response: UserResponse = {
  id: 1,
  name: "김철수",
  email: "kim@example.com"
  // password 없어도 OK (빠졌으니까)
};

console.log(response);

실행 결과:

✓ 컴파일 성공
{ id: 1, name: '김철수', email: 'kim@example.com' }

민감한 정보를 제외한 응답 타입을 만들 수 있습니다

여러 속성 제외

type PublicUser = Omit<User, "password" | "email">;

const publicUser: PublicUser = {
  id: 1,
  name: "김철수"
};

console.log(publicUser);

실행 결과:

✓ 컴파일 성공
{ id: 1, name: '김철수' }

여러 속성을 동시에 제외할 수 있습니다

typescript guide 09 utility types

Record<K, T> - 키-값 쌍의 객체 타입

객체의 키와 값 타입을 지정할 수 있어요.

type Role = "admin" | "user" | "guest";

const permissions: Record<Role, string[]> = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"],
  guest: ["read"]
};

console.log(permissions.admin);
console.log(permissions.user);

실행 결과:

✓ 컴파일 성공
[ 'read', 'write', 'delete' ]
[ 'read', 'write' ]

정확한 키 집합과 값 타입을 지정할 수 있습니다

페이지 설정 예제

type Page = "home" | "about" | "contact";

interface PageConfig {
  title: string;
  path: string;
}

const pages: Record<Page, PageConfig> = {
  home: { title: "홈", path: "/" },
  about: { title: "소개", path: "/about" },
  contact: { title: "연락", path: "/contact" }
};

console.log(pages.home);

실행 결과:

✓ 컴파일 성공
{ title: '홈', path: '/' }

모든 페이지가 동일한 구조를 가지도록 강제합니다

Exclude<T, U> - 유니온 타입에서 제외

유니온 타입에서 특정 타입을 제거해요.

type AllTypes = string | number | boolean;
type StringAndNumber = Exclude<AllTypes, boolean>;

const value1: StringAndNumber = "hello";
const value2: StringAndNumber = 123;
// const value3: StringAndNumber = true;  // 에러! boolean 제외됨

실행 결과:

✗ 타입 에러
Type 'boolean' is not assignable to type 'StringAndNumber'

boolean 타입이 제외되었습니다

Extract<T, U> - 유니온 타입에서 추출

Exclude의 반대예요. 특정 타입만 추출합니다.

type AllTypes = string | number | boolean;
type OnlyString = Extract<AllTypes, string>;

const str: OnlyString = "hello";
// const num: OnlyString = 123;  // 에러! string만 허용

실행 결과:

✗ 타입 에러
Type 'number' is not assignable to type 'string'

string 타입만 추출되었습니다

NonNullable - null과 undefined 제거

null과 undefined를 제외한 타입을 만들어요.

type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;

const str: DefiniteString = "hello";
// const nullStr: DefiniteString = null;       // 에러!
// const undefinedStr: DefiniteString = undefined;  // 에러!

실행 결과:

✗ 타입 에러
Type 'null' is not assignable to type 'string'

null과 undefined가 제거되었습니다

ReturnType - 함수 반환 타입 추출

함수의 리턴 타입을 가져와요.

function createUser() {
  return {
    id: 1,
    name: "김철수",
    email: "kim@example.com"
  };
}

type User = ReturnType<typeof createUser>;

const user: User = {
  id: 2,
  name: "이영희",
  email: "lee@example.com"
};

console.log(user);

실행 결과:

✓ 컴파일 성공
{ id: 2, name: '이영희', email: 'lee@example.com' }

함수 반환 타입을 재사용할 수 있습니다

Parameters - 함수 매개변수 타입 추출

함수의 매개변수 타입을 튜플로 가져와요.

function createUser(name: string, age: number, email: string) {
  return { name, age, email };
}

type CreateUserParams = Parameters<typeof createUser>;

const params: CreateUserParams = ["김철수", 25, "kim@example.com"];
const user = createUser(...params);

console.log(user);

실행 결과:

✓ 컴파일 성공
{ name: '김철수', age: 25, email: 'kim@example.com' }

함수 매개변수 타입을 배열로 재사용했습니다

실전 예제: 폼 상태 관리

유틸리티 타입을 조합해서 실용적인 타입 만들기:

interface FormData {
  username: string;
  email: string;
  password: string;
  confirmPassword: string;
  age: number;
}

// 전송용: password 제외
type FormSubmit = Omit<FormData, "confirmPassword">;

// 에러 상태: 모든 필드 선택적
type FormErrors = Partial<Record<keyof FormData, string>>;

// 읽기 전용 폼 데이터
type ReadonlyFormData = Readonly<FormData>;

const submitData: FormSubmit = {
  username: "kimcs",
  email: "kim@example.com",
  password: "1234",
  age: 25
};

const errors: FormErrors = {
  email: "이메일 형식이 잘못되었습니다",
  password: "비밀번호가 너무 짧습니다"
};

console.log("전송 데이터:", submitData);
console.log("에러:", errors);

실행 결과:

✓ 컴파일 성공
전송 데이터: { username: 'kimcs', email: 'kim@example.com', password: '1234', age: 25 }
에러: { email: '이메일 형식이 잘못되었습니다', password: '비밀번호가 너무 짧습니다' }

유틸리티 타입을 조합하여 실용적인 폼 타입을 만들었습니다

정리하면

  • Partial: 모든 속성 선택적으로
  • Required: 모든 속성 필수로
  • Readonly: 모든 속성 읽기 전용으로
  • Pick: 특정 속성만 선택
  • Omit: 특정 속성 제외
  • Record: 키-값 객체 타입
  • Exclude: 유니온에서 제외
  • Extract: 유니온에서 추출
  • NonNullable: null/undefined 제거
  • ReturnType: 함수 반환 타입 추출
  • Parameters: 함수 매개변수 타입 추출

운영자 실전 노트

실제 프로젝트 진행하며 겪은 문제

  • 유틸리티 타입을 모르고 수동으로 타입 복사하다가 중복 코드 양산 → Partial, Omit 등으로 간결하게 해결
  • Pick과 Omit을 동시에 사용하다가 타입이 꼬여서 헷갈림 → 하나만 사용하거나 명확히 분리해야 함

이 경험을 통해 알게 된 점

  • 유틸리티 타입은 TypeScript의 숨겨진 보물이다. 익숙해지면 생산성이 2배 이상 향상됨
  • Partial과 Pick/Omit을 적절히 조합하면 거의 모든 타입 변환이 가능하다

다음 글에서는 실전 프로젝트를 만든다. TypeScript로 Todo 앱을 만들고, React와 함께 사용하는 방법까지 다룬다.


다음 글 보기

← 이전 글
TypeScript 독학 가이드 8 - 제네릭
다음 글 →
TypeScript 독학 가이드 10 - 실전 프로젝트
← 블로그 목록으로