이 글은 Chat GPT로 TypeScript를 공부하며 정리한 글이다.
실무에서 API 응답 타입은 다음과 같이 깊고 중첩된 구조를 갖기 쉽다.
interface ApiResponse {
status: string;
data: {
user: {
id: number;
profile: {
name: string;
email: string;
};
};
posts: {
id: number;
title: string;
}[];
};
}
하지만 실제 <form>에서는 이런 깊은 구조를 그대로 다루기 어렵다.
user.profile.name"setValue("user.profile.email", ...)이 과정을 매번 수동으로 작성하면 반복적이며, 타입 안정성도 낮다.
목표:
하나의 API 타입 → 폼 구조 / 검증 / 서버 전송 모델까지 자동으로 생성
API 응답은 보통 data 안에 핵심 값이 있다.
이를 자동으로 추출하는 타입이 필요하다.
type DeepExtract<T> =
T extends Promise<infer U> ? DeepExtract<U> :
T extends { data: infer D } ? DeepExtract<D> :
T;
적용:
type Data = DeepExtract<ApiResponse>;
결과:
/*
{
user: { id: number; profile: { name: string; email: string } };
posts: { id: number; title: string }[];
}
*/
폼 필드 이름은 "user.profile.name"처럼 문자열 경로가 적합하다.
type IsPlainObj<T> = T extends object
? T extends any[]
? false
: T extends Function
? false
: true
: false;
✔ 배열/함수/Date 같은 타입이 flatten되는 것을 방지
type Flatten<T, P extends string = ''> = {
[K in keyof T & string]:
IsPlainObj<T[K]> extends true
? Flatten<T[K], `${P}${K}.`>
: { [key in `${P}${K}`]: T[K] };
}[keyof T & string];
적용:
type FormFields = Flatten<DeepExtract<ApiResponse>>;
결과:
/*
{
'user.id': number;
'user.profile.name': string;
'user.profile.email': string;
'posts': { id: number; title: string }[];
}
*/
Flatten은 조각 객체들의 유니온을 생성한다.
예: { 'user.id': number } | { 'user.profile.name': string } | ...
이를 하나의 객체로 합쳐야 한다.
type UnionToIntersection<U> =
(U extends any ? (x: U) => void : never) extends
(x: infer I) => void ? I : never;
Flat<T> — 평탄화 객체 완성type Flat<T> = UnionToIntersection<Flatten<T>>;
type ExpandType<T> = UnionToIntersection<{
[K in keyof T & string]:
K extends `${infer Head}.${infer Tail}`
? { [P in Head]: ExpandType<{ [Q in Tail]: T[K] }> }
: { [P in K]: T[K] }
}[keyof T & string]>;
적용 예:
type Expanded = ExpandType<{
'user.profile.name': string;
'user.profile.email': string;
}>;
결과 (병합 포함):
/*
{
user: {
profile: {
name: string;
email: string;
};
};
}
*/
type FormMapper<T> = {
form: Flat<DeepExtract<T>>;
response: DeepExtract<T>;
expand: ExpandType<Flat<DeepExtract<T>>>;
};
type UserForm = FormMapper<ApiResponse>["form"];
function UserProfileForm() {
const [form, setForm] = useState<Partial<UserForm>>({});
return (
<>
<input
name="user.profile.name"
value={form["user.profile.name"] ?? ""}
onChange={e =>
setForm(prev => ({
...prev,
"user.profile.name": e.target.value
}))
}
/>
<input
name="user.profile.email"
value={form["user.profile.email"] ?? ""}
onChange={e =>
setForm(prev => ({
...prev,
"user.profile.email": e.target.value
}))
}
/>
</>
);
}
type ApiData = ExpandType<Flat<DeepExtract<ApiResponse>>>;
| 시나리오 | 설명 |
|---|---|
| Form 라이브러리 | RHF/Formik의 name 자동 생성 |
| DTO 자동 생성 | 서버 응답 → Form 모델 자동 변환 |
| 역매핑 | Form 데이터 → 서버 전송 구조 자동 감싸기 |
| Validation | 필드 단위 타입 기반 검증 자동화 |
"API 타입 하나로 form 구조·검증·역매핑까지 자동화한다."