[타입스크립트] 유틸리티 타입으로 타입 선언 유연하게 하기

Woonil·2025년 8월 31일
0

타입스크립트

목록 보기
2/4
post-thumbnail

Utility Types(유틸리티 타입)이란 제네릭, 맵드 타입, 조건부 타입 등의 타입 조작 기능을 이용해 이후 자주 사용되는 타입을 미리 만들어 놓은 것이다. 즉, 개발자는 복잡한 타입 로직을 일일이 정의할 필요가 없고, 제공되는 키워드를 사용하기만 하면 된다. 이를 통해 타입 코드의 재활용성을 높이고 정의를 간결하게 만들 수 있다.

‘한 입 크기로 잘라먹는 타입스크립트 - 이정환’ 강의를 주로 참고하여 개념을 정리하였다.

🤔개념

맵드 타입 기반

// 아래 설명에서 사용될 Post 타입
interface Post {
  title: string;
  tags: string[];
  content: string;
  thumbnailURL?: string;
}

Partial<T>

프로퍼티를 optional로 변환하여 일부만 사용이 가능하게끔 한다. 즉, 객체 타입의 모든 속성을 선택적으로 만드는 것이다.

// 내부 구현
type Partial<T> = {
  [key in keyof T]?: T[key];
};

const draft: Partial<Post> = {
  title: "제목",
  content: "초안",
};
interface User {
    id: number;
    name: string;
    age?: number;
    gender?: "m" | "f";
}

let admin: Partial<User> = {
    id: 1,
    name: "Bob",
    job: "" // Error
}

Required<T>

특정 객체 타입의 모든 프로퍼티를 필수 프로퍼티로 바꿔주어 모든 프로퍼티를 사용해야만 하게 바꿔준다. 즉, 객체 타입의 모든 속성을 필수적으로 만드는 것이다.

// 내부 구현
ype Required<T> = {
  [key in keyof T]-?: T[key];
};

const withThumbnail: Post = {
  title: "한입 타스",
  tags: ["ts"],
  content: "",
  thumbnailURL: "http",
};
interface User {
    id: number;
    name: string;
    age?: number;
    gender?: "m" | "f";
}

let admin: Required<User> = {
    id: 1,
    name: "Bob",
    age: 30,
    gender: "m"
}

Readonly<T>

객체 타입에서 모든 프로퍼티를 읽기 전용 프로퍼티로 만들어 수정 불가능하게 한다.

// 내부 구현
type Readonly<T> = {
  readonly [key in keyof T]: T[key];
};

const readonlyPost: Readonly<Post> = {
  title: "보호된 게시글",
  tags: [],
  content: "",
};

// Cannot assign to 'content' because it is a read-only property.
readonlyPost.content = "";
let admin: Readonly<User> = {
    id: 1,
    name: "Bob",
    age: 30,
    gender: "m"
}

admin.id = 2; // Error

Pick<T, K>

기존 타입에서 원하는 속성만 선택해서 새 타입을 만든다.

// 내부 구현
type Pick<T, K extends keyof T> = {
  // K extends 'title' | 'tags' | 'content' | 'thumbnailURL'
  [key in K]: T[key];
};

const legacyPost: Pick<Post, "title" | "content"> = {
  title: "옛날 글",
  content: "옛날 컨텐츠",
};
// Type 'number' does not satisfy the constraint 'keyof Post'.
const legacyPost: Pick<Post, number> = {
  title: "옛날 글",
  content: "옛날 컨텐츠",
};
const admin: Pick<User, "id" | "name"> = {
    id: 0,
    name: "Bob",
}

Omit<T, K>

Pick으로 넘어오는 키(K)가 많아질 경우 코드가 길어질 수 있으므로 이럴때는 반대로 제외하는 것이 코드가 간결해질 수 있다. Omit은 Pick과 반대로 객체 타입으로부터 특정 속성을 제외하여 새 타입을 만든다.

Omit: 생략하다, 빼다

// 내부 구현
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

const noTitlePost: Omit<Post, "title"> = {
  content: "",
  tags: [],
  thumbnailURL: "",
};
// T = Post, K = 'title'
// Pick<Post, Exclude<keyof Post, 'title'>>
// Pick<Post, Exclude<'title' | 'content' | 'tags' | 'thumbnailURL', 'title' >>
// Pick<Post, 'content' | 'tags' | 'thumbnailURL' >
const admin: Omit<User, "age" | "gender"> = {
    id: 0,
    name: "Bob",
}

Record<K, V>

특정 키와 값 타입의 동일한 패턴을 가지는 사전 형태의 객체 타입을 쉽게 정의할 수 있다. 인덱스 시그니처와 비슷하지만 키 타입을 보다 구체적으로 제한할 수 있다는 점에서 차이를 보인다.

type Thumbnail = {
  large: { url: string };
  medium: { url: string };
  small: { url: string };
};

type ThumbnailRecord = Record<"large" | "medium" | "small", { url: string }>;

// 내부 구현
type Record<K extends keyof any, V> = {
  [key in K]: V;
};
type Grade = "1" | "2" | "3"
type Score = "A" | "B" | "C" | "D"

const score: Record<Grade, Score> = {
    1: "A",
    2: "B",
    3: "C",
}
interface User {
    id: number;
    name: string;
    age: number;
}

function isValid(user: User) {
    const result: Record<keyof User, boolean> = {
        id: user.id > 0,
        name: user.name !== "",
        age: user.age > 0,
    };
    
    return result;
}

조건부 타입 기반

Exclude<T, U>

특정 타입을 제거한다.

// 내부 구현
type Exclude<T, U> = T extends U ? never : T;
// 1.
// Exclude<string, boolean> | Exclude<boolean, boolean>
// 2.
// string | never
// 3.
// string

type A = Exclude<string | boolean, boolean>; // string
type T1 = string | number | boolean;
type T2 = Exclude<T1, number | string>;

Extract<T, U>

특정 타입을 추출한다.

// 내부 구현
type Extract<T, U> = T extends U ? T : never;

type B = Extract<string | boolean, boolean>; // boolean

ReturnType<T>

함수의 반환 타입을 특정한다.

// 내부 구현
type ReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : never;

function funcA() {
  return "hi";
}
function funcB() {
  return 10;
}

// () => string 가 T로 넘어감 -> R이 string으로 추론됨
type ReturnA = ReturnType<typeof funcA>;
// () => number 가 T로 넘어감 -> R이 number로 추론됨
type ReturnB = ReturnType<typeof funcA>;

기타

NonNullable

null, undefined 제외

type T1 = string | null | undefined | void;
type T2 = NonNullable<T1>;

😎실습

Pick으로 회원가입 특정 단계에서 필요한 필드만 추출하기

프로젝트 wangnOOni의 회원가입 폼은 funnel로 구현했었으며, 각 Step(단계)마다 별도의 파일에서 상태가 관리되고 있었다. 이때, 각 필드에 대한 타입을 지정해야 했다. 회원가입 폼에 필요한 입력값들이 정의된 IAccountSetupData 라는 타입에서 하나의 필드에 대한 타입만이 필요했고, Pick 이 가장 적절하다고 판단했다.

// 회원가입 폼 모든 입력 파라미터
export interface IAccountSetupData {
  username: string;
  email: string;
  password: string;
  verificationToken: string;
}

// Step 형식의 회원가입 진행 절차 중 특정 절차에서 필요한 필드값의 타입을 추출
import { IAccountSetupData } from "../../../types/type";
type IFieldValues = Pick<IAccountSetupData, "username">;

Omit으로 비스타일링 요소는 제외하고 Styled-Componets에서 재사용하기

export interface IIconProps {
  size?: string;
  color?: string;
  icon: IconType; // 이름 그대로 아이콘 타입 명시(css 속성x)
}

const IconWrapper = styled.div<**Omit<IIconProps, "icon"**>>`
  display: flex;
  ...
`;

Record로 여러 테마의 CSS 적용하기

type PrimaryButtonTheme = "dark" | "light" | "social" | "text";

const dark = "bg-primary text-white";
const light = "bg-white text-primary";
const social = "bg-social text-white";

const color: Record<PrimaryButtonTheme, string> = {
  dark,
  light,
  social,
};

interface IPrimaryButtonProps {
  theme: PrimaryButtonTheme;
}
export default function PrimaryButton({
  theme,
}: IPrimaryButtonProps & { theme: PrimaryButtonTheme }) {
  return (
    <button
      className={`w-full h-[59px] ${color[theme]} rounded-primary-button`}
    >
      {children}
    </button>
  );
}
profile
프론트 개발과 클라우드 환경에 관심이 많습니다:)

0개의 댓글