Typescript day3

dongEon·2025년 8월 26일
0
post-thumbnail

Day 3에서는 실무 스타일 패턴 학습을 주제로, 단순 문법 암기에서 벗어나 실제 프론트엔드 개발에서 자주 맞닥뜨리는 타입 문제를 연습했습니다.
이 글은 문제 풀이 과정과, 풀면서 생긴 궁금증을 정리한 Q&A 심화 정리까지 모두 담고 있습니다.


Problem 1 – FilterString

문제 요구

주어진 타입 T가 string이면 그대로 남기고, 그렇지 않으면 never가 되도록 타입을 정의하라.

문제 code

// 초기 상태  
export type FilterString<T> = never;  

type A = FilterString<string>;      // string  
type B = FilterString<number>;      // never  
type C = FilterString<"hello">;     // "hello"  
type D = FilterString<boolean>;     // never  
type E = FilterString<string | 1>;  // string  

test code

테스트 코드 보기
import type { Expect, Equal } from "../../tests/type-assert.ts";  
import type { FilterString } from "./01-filter-string.ts";  

type _cases = [  
  Expect<Equal<FilterString<string>, string>>,  
  Expect<Equal<FilterString<number>, never>>,  
  Expect<Equal<FilterString<"hello">, "hello">>,  
  Expect<Equal<FilterString<boolean>, never>>,  
  Expect<Equal<FilterString<string | 1>, string>>,  
];  

풀이 과정

조건부 타입을 떠올렸고, T extends string ? T : never 형태로 구현하면 정확히 요구사항을 만족했습니다.
유니언 타입 입력 시 분배 조건부 타입이 동작해 자동으로 필터링된 결과가 나왔습니다.

적용된 개념

  • 조건부 타입 (T extends U ? X : Y)
  • 분배 조건부 타입

최종 코드

export type FilterString<T> = T extends string ? T : never;  

Problem 2 – PublicUser

문제 요구

User 타입에서 password와 ssn만 제거한 PublicUser 타입을 만들어라.

문제 code

export type User = {  
  id: string;  
  email: string;  
  name: string;  
  phone?: string;  
  address?: string;  
  password: string;  
  ssn: string;  
};  

// 초기 상태  
export type PublicUser = never;  

test code

테스트 코드 보기
import type { Expect, Equal } from "../../tests/type-assert.ts";  
import type { PublicUser } from "./02-public-user.ts";  

type _cases = [  
  Expect<Equal<PublicUser,  
    { id: string; email: string; name: string; phone?: string; address?: string }  
  >>,  
];  

풀이 과정

민감 정보를 제거한다는 요구는 Omit 유틸리티 타입으로 쉽게 해결 가능했습니다.
Omit<User, "password" | "ssn">로 정의하면 의도대로 동작합니다.

적용된 개념

  • Omit<T, K> = 특정 키 제거
  • 내부적으로 Pick + Exclude 조합으로 동작

최종 코드

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

Problem 3 – StatusCounters

문제 요구

네트워크 요청 상태를 "idle" | "loading" | "success" | "error"로 정의하고,
각 상태별 카운터를 number 값으로 갖는 객체 타입을 정의하라.

문제 code

// 초기 상태  
export type Status = string;  
export type Counters = Record<string, number>;  

test code

테스트 코드 보기
import type { Expect, Equal } from "../../tests/type-assert.ts";  
import type { Status, Counters } from "./03-status-counters.ts";  

type _cases = [  
  Expect<Equal<Status, "idle" | "loading" | "success" | "error">>,  
  Expect<Equal<Counters, { idle: number; loading: number; success: number; error: number }>>,  
];  

풀이 과정

매핑 타입으로 유니언을 순회하며 모든 상태를 key로 정의했습니다.
Record<Status, number>로 간단히 해결할 수 있었습니다.

적용된 개념

  • 매핑 타입 { [K in U]: ... }
  • Record<K, V> = { [P in K]: V }

최종 코드

export type Status = "idle" | "loading" | "success" | "error";  
export type Counters = Record<Status, number>;  

Problem 4 – ApiValues

문제 요구

ApiMap 구조에서 각 엔드포인트의 data 타입만 추출하여 유니언으로 모으라.

문제 code

export type ApiMap = {  
  "/me": { data: { id: string; name: string } };  
  "/items": { data: Array<{ id: string; price: number }> };  
  "/health": { data: { ok: true } };  
};  

// 초기 상태  
export type ValueOf<T> = never;  
export type ApiValues<T> = never;  

test code

테스트 코드 보기
import type { Expect, Equal } from "../../tests/type-assert.ts";  
import type { ApiMap, ApiValues } from "./04-api-values.ts";  

type _cases = [  
  Expect<Equal<  
    ApiValues<ApiMap>,  
    { id: string; name: string }  
    | Array<{ id: string; price: number }>  
    | { ok: true }  
  >>,  
];  

풀이 과정

객체 value 타입 전체를 뽑는 T[keyof T]를 ValueOf로 정의했습니다.
그 후 ValueOf["data"]로 각 엔드포인트의 data 속성 타입만 추출했습니다.

적용된 개념

  • 인덱스드 액세스 타입 T[K]
  • T[keyof T] = 객체 value 전체 유니언
  • ValueOf["data"]로 data 속성 추출

최종 코드

export type ValueOf<T> = T[keyof T];  
export type ApiValues<T> = ValueOf<T>["data"];  

Problem 5 – PartialBy

문제 요구

PartialBy<T, K>를 구현해, T에서 K 키만 optional로 만들고 나머지는 그대로 유지하라.

문제 code

type User = { id: string; name: string; email: string; age?: number };  

// 초기 상태  
export type PartialBy<T, K extends keyof T> = never;  

test code

테스트 코드 보기
import type { Expect, Equal } from "../../tests/type-assert.ts";  
import type { PartialBy } from "./05-partial-by.ts";  

type User = { id: string; name: string; email: string; age?: number };  

type _cases = [  
  Expect<Equal<  
    PartialBy<User, "email">,  
    { id: string; name: string; email?: string; age?: number }  
  >>,  
  Expect<Equal<  
    PartialBy<User, "name" | "age">,  
    { id: string; name?: string; email: string; age?: number }  
  >>,  
];  

풀이 과정

처음엔 조건부 타입으로 optional을 구현했지만 이는 단순히 undefined 허용으로만 처리되었습니다.
그래서 Omit + Pick + Partial을 교차(&)시켜 특정 키만 optional로 만들었습니다.

적용된 개념

  • Omit<T, K>: 특정 키 제외
  • Pick<T, K>: 특정 키만 선택
  • Partial<Pick<T, K>>: 해당 키만 optional 처리
  • & (교차 타입): 두 타입을 합쳐 최종 타입 생성

최종 코드

export type PartialBy<T, K extends keyof T> =  
  Omit<T, K> & Partial<Pick<T, K>>;  

🔎 Q&A 심화 정리


1. K in Status vs keyof Status

(1) K in Status

  • in 키워드는 매핑 타입 문맥에서 사용된다.
  • Status라는 유니언 타입의 원소들을 하나씩 순회하며, 그 값을 객체의 키로 만든다.
    type Status = "idle" | "loading" | "success";  
    type Counters = { [K in Status]: number };  

    // 결과  
    type Counters = {  
      idle: number;  
      loading: number;  
      success: number;  
    }  

👉 즉, K in Status유니언을 그대로 key 집합으로 사용하는 방법이다.

(2) keyof Status

  • keyof객체 타입의 키를 뽑아내는 연산자다.
  • 하지만 Status는 유니언 리터럴 타입이지 객체가 아니다.
  • 따라서 keyof Status를 쓰면 문자열 리터럴의 내부 프로퍼티가 뽑혀서 전혀 다른 결과가 된다.
type Status = "idle" | "loading";  
type Wrong = keyof Status;  
// 결과: string | number | symbol  

👉 이유: 문자열 리터럴 타입은 사실상 string의 하위 타입이고, 자바스크립트에서 문자열의 키는 string | number | symbol이 될 수 있기 때문이다.

(3) 정리

  • 유니언 타입을 그대로 키로 쓰고 싶을 때K in Status
  • 객체의 키를 추출하고 싶을 때keyof User

2. keyof 객체 타입 결과는?

(1) 기본 객체

type User = { id: string; name: string; email?: string };  
type Keys = keyof User;  
// "id" | "name" | "email"  

👉 옵셔널 여부와 관계없이 키는 모두 포함된다.

(2) 인덱스 시그니처

type Dictionary = { [key: string]: number };  
type Keys = keyof Dictionary;  
// string | number  

👉 자바스크립트 객체 키는 사실상 string | number | symbol이 될 수 있다.

(3) 배열

type Arr = string[];  
type Keys = keyof Arr;  
// number | "length" | "push" | "pop" | ...  

👉 배열도 사실상 객체이므로, 숫자 인덱스와 배열 메서드들이 키로 잡힌다.

(4) 실무 응용

  1. 안전한 프로퍼티 접근

      function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {  
        return obj[key];  
      }  
  2. 자동 필드 검사기

      type UserFields = keyof User;  
      // "id" | "name" | "email"  

3. & (교차 타입, Intersection Type)

(1) 기본 개념

  • A & B는 A와 B를 모두 만족하는 타입이다.
  • 객체 타입끼리 교차하면 속성이 병합된다.
    type A = { id: number };  
    type B = { name: string };  
    type AB = A & B;  
    // { id: number; name: string }  

(2) 겹치는 키가 다르면?

type A = { value: string };  
type B = { value: number };  
type AB = A & B;  
// { value: string & number } → never  

👉 교차 타입에서 겹치는 키의 타입이 호환되지 않으면 never가 된다.

(3) 실무 응용

  • 유틸리티 타입 합성
    type PartialBy<T, K extends keyof T> =  
      Omit<T, K> & Partial<Pick<T, K>>;  
  • API 응답 타입 확장
    type Base = { id: string };  
    type WithTimestamp = { createdAt: Date };  
    type ApiResponse = Base & WithTimestamp;  
    // { id: string; createdAt: Date }  

4. 인덱스드 액세스 타입 vs 매핑 타입 문맥

(1) 인덱스드 액세스 타입

  • 문법: T[K]
  • 의미: 타입 T에서 키 K의 값 타입을 가져온다.
    type User = { id: number; name: string };  
    type IdType = User["id"]; // number  
    type Values = User[keyof User]; // number | string  

👉 핵심: 값을 꺼내는 문법이다.

(2) 매핑 타입 문맥

  • 문법: { [K in U]: ... }
  • 의미: 유니언 U를 순회해서 새로운 객체 타입을 만든다.
    type Status = "idle" | "loading";  
    type Counters = { [K in Status]: number };  
    // { idle: number; loading: number }  

👉 핵심: 키를 새로 만드는 문법이다.

(3) 키 리매핑(as)

  • 매핑 타입 문맥에서만 사용할 수 있다.
    type Rename<T> = {  
      [K in keyof T as `new_${string & K}`]: T[K];  
    };  

👉 인덱스드 액세스(T[keyof T])에서는 as를 쓸 수 없다.

(4) 정리

  • T[K] → 값 꺼내기
  • { [K in U]: ... } → 키 순회하여 객체 만들기
  • as (키 리매핑) → 매핑 타입에서만 사용 가능

✨ Day 3 전체 정리

  • 조건부 타입으로 타입 필터링
  • Omit/Pick/Partial 조합으로 일부 키 optional 처리
  • 매핑 타입과 Record로 상태 매핑
  • Indexed Access + ValueOf로 API 응답 타입 추출
  • 교차 타입(&)으로 유틸리티 합성
  • K in vs keyof, 인덱스드 액세스 vs 매핑 타입 문맥, ?: vs | undefined 차이

👉 Day3 학습은 단순 문법을 넘어서, 실무 타입 설계 감각을 키우는 훈련이었다.

profile
개발 중에 마주한 문제와 해결 과정, 새롭게 배운 지식, 그리고 알고리즘 문제 해결에 대한 다양한 인사이트를 공유하는 기술 블로그입니다

0개의 댓글