MoEasy - 타입스크립트 스터디(2주차) - [유틸리티 타입, 타입 추출, 타입 재사용]

연도·2025년 2월 7일

JavaScript, TypeScript

목록 보기
6/8
post-thumbnail

들어가기 전 간단 회고

타입스크립트와 더더욱 친숙해지기!!
스터디의 기본 개념을 바탕으로 개인적으로 꾸준하게 공부해야겠다!
실질적으로 특히 더 테스트 코드 짜면서 타입을 완전하게 지켜야 한다. 그 과정에서 당연히 타입스크립트를 더 정확하게! 알아야 한다.! 그래야 지저분하지 않은 정당한 코드를 짤 수 있다~

기본적인 타입 조작

typeof frutits[number]

typeof는 자바스크립트 값을 타입스크립트 타입으로 변환

[number]은 배열/튜플의 모든 값을 유니온 타입으로 추출함.

유틸리티 타입들

Extract

type Extract<T, U> = T extends U ? T : never

Union 타입에서 조거에 맞는 멤버만 추출함.

특정 형태로 끝나거나 포함된 값만 남겨서 새로운 타입을 만들 때 주로 사용됨.

실무에선 상태값 or 특정 접미사/접두사가 붙은 API 응답만 골라낼 때 활용됨.

Pick

type Pick<T, K extends keyof T> = { [P in K]: T[P]; }

객체 타입에서 필요한 속성만 추출해서 새 타입을 생성함.

DB 조회 시 or API 응답에서 꼭 필요한 필드만 선택해서 쓸 때 가장 많이 쓰인다.

실무에서는 DTO 정의할 때 자주 등장함(특히나 요청 바디 타입을 만들 때)

Omit

type Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P]; }

특정 속성을 제외한 나머지 필드로 타입 생성함.

기존 타입에서 불필요한 필드를 빼서 간결한 타입을 만들거나 Patch용 타입을 만들 때 필수이다.

실무에서는 제외한 상태에서 재정의 → 커스텀 DTO 작성시에 매우 유용함.

Record

type Record<K extends keyof any, T> = { [P in K]: T; }

키 K와 타입 T로 객체 타입 생성

특정 키들의리스트에 일괄적으로 동일한 값 타입을 지정함.

동적으로 키가 정해지고 값이 동일 타입일 때 안전하게 객체를 만들 수 있다.

Required

type Required<T> = { [P in keyof T]-?: T[P]; }

모든 선택적 속성을 필수로 변경

기존 타입을 기반으로 특정 필드를 강제화할 때 유용하다.

Capitalize / Uppercase

첫 글자를 대문자로, 모든 글자를 대문자로

문제1

/* _____________ 테스트 케이스 _____________ */
import type { Equal, Expect, ExpectExtends } from "@type-challenges/utils";
/*
  과제 1 template union types
*/
/* _____________ 여기에 코드 입력 _____________ */
const fruits = ["apple", "banana", "orange"] as const;
const freshness = ["ripe", "unripe", "fresh"] as const;
const sizes = ["small", "medium", "large"] as const;

/**
 * 1-1 과일-신선도-크기 를 나타내는 문자 타입
 * 위 배열들은 과일 종류, 신선도와 크기를 관리하는 배열입니다.
 * 위 배열들을 이용해 '과일-신선도-크기' 만을 가리키는 타입을 만들어보시오.
 */
// 해설
// ``(템플릿 리터럴)을 사용해서 문자열 조합 타입을 만들고, 배열 요소 타입인 typeof fruits[number] 형태로 가져와서
// 각각 유니온 타입으로 추출한 후에 그것을을 연결해서 "과일-신선도-크기" 형태의 조합 타입으로 만든다.
type VariousFruits = `${typeof fruits[number]}-${typeof freshness[number]}-${typeof sizes[number]}`;
type VariousFruits = `${(typeof fruits)[number]}-${(typeof freshness)[number]}-${(typeof sizes)[number]}`;

/**
 * 1-2 VariousFruits에서 파생되어 크기가 small인 타입
 * 1-1에서 만든 타입에서 크기가 small인 string union을 추출하고 싶다.
 * 위 배열을 내용을 조작하지 않은 상태로 타입을 만들어보시오.
 * 정답은 하나만 있지 않으니 다양한 정답을 들고오실 수록 학습에 도움이 될 것이다.
 */
// 해설
// 첫번째 Template Literal Type를 직접 사용함. 1-1번과 동일
// 두번째 Extract 유틸리티 타입을 활용해 기존 VariousFruits 타입 중에서 small로 끝나는 문자열 조합만 뽑아오는 방식. 
type ExtractSmallSize = `${(typeof fruits)[number]}-${(typeof freshness)[number]}-small`;
type ExtractSmallSize2 = Extract<VariousFruits, `${string}-${string}-small`>;

/**
 * 1-3 아래 함수를 완성시켜서 신선도, 과일, 크기를 입력받았을 때 반환값이 string이 아닌
 * '과일-신선도-크기'인 타입이 유지되도록 구현하시오.
 * 크기는 선택 옵션으로 크기를 입력하지 않으면 small을 사용하도록 해보자.
 *
 * 필수 '과일-신선도-크기'
 * 선택 '{입력된 과일}-{입력된 신선도}-{입력된 신선도 || 'small'}'
 */
// 해설
// 선택도 함. 제네릭과 리터럴 타입 조합해서 정확한 타입 추론 가능해졌음.
// F는 fruits 배열의 요소, FR은 freshness 배열의 요소, S는 sizes 배열의 요소
function getFruits<
  F extends (typeof fruits)[number],
  FR extends (typeof freshness)[number],
  S extends (typeof sizes)[number] = "small"
>(fruit: F, fruitFreshness: FR, size: S = "small" as S) {
  return `${fruit}-${fruitFreshness}-${size}` as const;
}

/**
 * 1-4) 배열 자체는 그대로 둔채 과일 문자열의 첫번째 글자를 대문자로 바꾸고 싶다. 크기도 중요하니까 크기는 모두 대문자로 바꾸고싶다.
 * 과일 문자열만 첫번째 글자를 대문자로 바꾸고 나머지는 1-1에서 그대로인 '과일-신선도-크기' 타입을 만들어보시오.
 */
// 해설
// Capitalize, Uppercase 유틸리티 타입 사용
type CapitalizeFruitAndSize = `${Capitalize<(typeof fruits)[number]>}-${(typeof freshness)[number]}-${Uppercase<
  (typeof sizes)[number]
>}`;

/**
 * 1-5 제네릭으로 객체를 받아 size: 'size'의 유니온 타입 이라는 속성을 받으면 true가 되고
 * 이를 만족하지 못하면 false를 받게 만들어서 아래 테스트를 통과해보시오.
 */
// 해설
// extends를 사용해서 객체의 속성을 추출하고, 그 속성이 sizes 배열에 있는지 확인했음.
type HasSize<T> = T extends { size: (typeof sizes)[number] } ? true : false;

/* _____________ 테스트 케이스 _____________ */
/** 1-1 과제 */
type firstCases = [
  Expect<Equal<ExpectExtends<VariousFruits, "apple-ripe-medium">, true>>,
  Expect<Equal<ExpectExtends<VariousFruits, "banana-unripe-large">, true>>
];
/** 1-2 과제 */
type secondCases = [
  Expect<Equal<ExpectExtends<ExtractSmallSize, "apple-ripe-small">, true>>,
  Expect<Equal<ExpectExtends<ExtractSmallSize, "banana-unripe-large">, false>>
];

/** 1-3 과제: 튜플이 되도록 과제를 수행하면 됩니다. */
const ThirdCases = [getFruits("apple", "ripe"), getFruits("banana", "unripe", "large")] as const;

/** 1-3 추가 과제 */
// return 타입은 함수 타입의 반환 타입을 추출하는 유틸리티 함수.
// F는 apple라는 정확한 타입으로 고정, FR은 ripe라는 정확한 타입으로 고정, apple-ripe-small 특정 문자열 타입으로 고정됌.
type ThirdCasesAddition = [
  Expect<Equal<ReturnType<typeof getFruits<"apple", "ripe">>, "apple-ripe-small">>,
  Expect<Equal<ReturnType<typeof getFruits<"banana", "unripe", "large">>, "banana-unripe-large">>
];

/** 1-4 과제 */
type FourthCases = [
  Expect<Equal<ExpectExtends<CapitalizeFruitAndSize, "Apple-ripe-SMALL">, true>>,
  Expect<Equal<ExpectExtends<CapitalizeFruitAndSize, "Orange-unripe-LARGE">, true>>,
  Expect<Equal<ExpectExtends<CapitalizeFruitAndSize, "banana-unripe-large">, false>>
];

/** 1-5 과제 */
type fifthCases = [
  Expect<Equal<HasSize<{ size: "small" }>, true>>,
  Expect<Equal<HasSize<{ size: "big" }>, false>>,
  Expect<Equal<HasSize<{ size: "small" }>, true>>
];

문제2

/* _____________ 테스트 케이스 _____________ */
/*
    과제 2 기존 타입을 우아하게 재사용하기
*/
/* _____________ 여기에 코드 입력 _____________ */
type Keyword = unknown;
type Schedule = unknown;
type Member = unknown;
type Meeting = {
  meeting_id: number;
  name: string;
  explanation: string;
  limit: number;
  thumbnail: string;
  canJoin?: boolean;
  keywords: Keyword[];
  schedules: Schedule[];
  members: Member[];
  createdAt: Date;
  updatedAt: Date;
};
/** 어디서 본것같은 굉장히 익숙한 타입이다. 위 타입을 이용해 아래 제시된 타입을 구현하시오. */

/**
 * 2-1) 모임과 관련된 기능을 구현하기 위해 모임의 타입을 사용하려고 한다.
 * 하지만 모임의 이름과 설명만 있으면 되기 때문에 나머지 속성들은 고려하지 않은 타입을 만들려고 한다.
 * 유틸리티 타입을 사용하여 name과 explanation만 있는 새로운 타입을 만들어보시오.
 * 정답은 하나만 있지 않으니 다양한 정답을 들고오실 수록 학습에 도움이 될 것이다.
 */
/**
 * Pick는 Meeting 타입 중에서 name과 explanation을 골라서 뽑음.
 * Omit는 Meeting 타입 중에서 제외할 것들을 골라주고 그 이외에 남은것들로 새로운 타입을 만듬.
 * Record는 키값인 name과 explanation 에 대해서 직접 string라고 타입을 지정해줌. 왜냐? name과 explanation이 string이기 때문임.
 */
type MeetingNameAndExplanation = Pick<Meeting, "name" | "explanation">;
type MeetingNameAndExplanation2 = Omit<
  Meeting,
  "meeting_id" | "limit" | "thumbnail" | "canJoin" | "keywords" | "schedules" | "members" | "createdAt" | "updatedAt"
>;
type MeetingNameAndExplanation3 = Record<"name" | "explanation", string>;
/**
 * 2-2) 모임과 관련된 기능을 화면에서 추가하려고 한다.
 * 그런데 썸네일 이미지 주소와 인원수 제한을 선택적으로 받으려고 한다.
 * thumbnail과 limit이 옵셔널인 타입(다른 속성은 그대로)을 유틸리티 타입을 사용하여 만들어보시오.
 */
// Omit으로 기존에 필수였던 2개를 제외해주고,  다시 새롭게 ?를 붙여서 옵셔널로 만듬.
type MeetingThumbnailAndLimit = Omit<Meeting, "thumbnail" | "limit"> & {
  thumbnail?: string;
  limit?: number;
};

/**
 * 2-3) 모임의 가입 가능 여부(canJoin)을 선택적으로 받고 있었는데
 * 기존 타입을 그대로 둔채 canJoin을 필수옵션으로 가지고 있는 새로운 타입을 유틸리티 타입을 사용하여 만들어보시오.
 */
// 2-2와 같이 Omit 이용해서 기존 canJoin 제외하고, 다시 canJoin을 필수로 만들기.
type MeetingCanJoin = Omit<Meeting, "canJoin"> & { canJoin: boolean };
type MeetingCanJoin2 = Meeting & Required<Pick<Meeting, "canJoin">>;
profile
Software Engineer

0개의 댓글