
Day 3에서는 실무 스타일 패턴 학습을 주제로, 단순 문법 암기에서 벗어나 실제 프론트엔드 개발에서 자주 맞닥뜨리는 타입 문제를 연습했습니다.
이 글은 문제 풀이 과정과, 풀면서 생긴 궁금증을 정리한 Q&A 심화 정리까지 모두 담고 있습니다.
주어진 타입 T가 string이면 그대로 남기고, 그렇지 않으면 never가 되도록 타입을 정의하라.
// 초기 상태
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
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 형태로 구현하면 정확히 요구사항을 만족했습니다.
유니언 타입 입력 시 분배 조건부 타입이 동작해 자동으로 필터링된 결과가 나왔습니다.
export type FilterString<T> = T extends string ? T : never;
User 타입에서 password와 ssn만 제거한 PublicUser 타입을 만들어라.
export type User = {
id: string;
email: string;
name: string;
phone?: string;
address?: string;
password: string;
ssn: string;
};
// 초기 상태
export type PublicUser = never;
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">로 정의하면 의도대로 동작합니다.
export type PublicUser = Omit<User, "password" | "ssn">;
네트워크 요청 상태를 "idle" | "loading" | "success" | "error"로 정의하고,
각 상태별 카운터를 number 값으로 갖는 객체 타입을 정의하라.
// 초기 상태
export type Status = string;
export type Counters = Record<string, number>;
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>로 간단히 해결할 수 있었습니다.
export type Status = "idle" | "loading" | "success" | "error";
export type Counters = Record<Status, number>;
ApiMap 구조에서 각 엔드포인트의 data 타입만 추출하여 유니언으로 모으라.
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;
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 속성 타입만 추출했습니다.
export type ValueOf<T> = T[keyof T];
export type ApiValues<T> = ValueOf<T>["data"];
PartialBy<T, K>를 구현해, T에서 K 키만 optional로 만들고 나머지는 그대로 유지하라.
type User = { id: string; name: string; email: string; age?: number };
// 초기 상태
export type PartialBy<T, K extends keyof T> = never;
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로 만들었습니다.
export type PartialBy<T, K extends keyof T> =
Omit<T, K> & Partial<Pick<T, K>>;
K in Status vs keyof StatusK in Statusin 키워드는 매핑 타입 문맥에서 사용된다. Status라는 유니언 타입의 원소들을 하나씩 순회하며, 그 값을 객체의 키로 만든다. type Status = "idle" | "loading" | "success";
type Counters = { [K in Status]: number };
// 결과
type Counters = {
idle: number;
loading: number;
success: number;
}
👉 즉, K in Status는 유니언을 그대로 key 집합으로 사용하는 방법이다.
keyof Statuskeyof는 객체 타입의 키를 뽑아내는 연산자다. Status는 유니언 리터럴 타입이지 객체가 아니다. keyof Status를 쓰면 문자열 리터럴의 내부 프로퍼티가 뽑혀서 전혀 다른 결과가 된다. type Status = "idle" | "loading";
type Wrong = keyof Status;
// 결과: string | number | symbol
👉 이유: 문자열 리터럴 타입은 사실상 string의 하위 타입이고, 자바스크립트에서 문자열의 키는 string | number | symbol이 될 수 있기 때문이다.
K in Status keyof User keyof 객체 타입 결과는?type User = { id: string; name: string; email?: string };
type Keys = keyof User;
// "id" | "name" | "email"
👉 옵셔널 여부와 관계없이 키는 모두 포함된다.
type Dictionary = { [key: string]: number };
type Keys = keyof Dictionary;
// string | number
👉 자바스크립트 객체 키는 사실상 string | number | symbol이 될 수 있다.
type Arr = string[];
type Keys = keyof Arr;
// number | "length" | "push" | "pop" | ...
👉 배열도 사실상 객체이므로, 숫자 인덱스와 배열 메서드들이 키로 잡힌다.
안전한 프로퍼티 접근
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
자동 필드 검사기
type UserFields = keyof User;
// "id" | "name" | "email"
& (교차 타입, Intersection Type)A & B는 A와 B를 모두 만족하는 타입이다. type A = { id: number };
type B = { name: string };
type AB = A & B;
// { id: number; name: string }
type A = { value: string };
type B = { value: number };
type AB = A & B;
// { value: string & number } → never
👉 교차 타입에서 겹치는 키의 타입이 호환되지 않으면 never가 된다.
type PartialBy<T, K extends keyof T> =
Omit<T, K> & Partial<Pick<T, K>>;
type Base = { id: string };
type WithTimestamp = { createdAt: Date };
type ApiResponse = Base & WithTimestamp;
// { id: string; createdAt: Date }
type User = { id: number; name: string };
type IdType = User["id"]; // number
type Values = User[keyof User]; // number | string
👉 핵심: 값을 꺼내는 문법이다.
type Status = "idle" | "loading";
type Counters = { [K in Status]: number };
// { idle: number; loading: number }
👉 핵심: 키를 새로 만드는 문법이다.
type Rename<T> = {
[K in keyof T as `new_${string & K}`]: T[K];
};
👉 인덱스드 액세스(T[keyof T])에서는 as를 쓸 수 없다.
K in vs keyof, 인덱스드 액세스 vs 매핑 타입 문맥, ?: vs | undefined 차이 👉 Day3 학습은 단순 문법을 넘어서, 실무 타입 설계 감각을 키우는 훈련이었다.