[TypeScript]
- [TS] TypeScript 심화 정리 - 시니어로 올라가기 위한 핵심 개념
시니어로 올라가기 위한 TypeScript 핵심 개념
1. infer
한 줄 요약: 타입 구조 안에서 타입을 꺼내는 것
1
2
3
4
5
| // Promise 안에서 타입 꺼내기
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type Result = Unwrap<Promise<string>>; // → string
type Result2 = Unwrap<string>; // → string (Promise 아니면 그냥 T)
|
1
2
3
4
5
6
7
8
9
| // 함수 리턴 타입 꺼내기
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
async function fetchUser(): Promise<{ id: number; name: string }> {
/* ... */
}
type UserData = MyReturnType<typeof fetchUser>;
// → Promise<{ id: number; name: string }>
|
왜 any랑 다르냐
any = 타입 추적 포기, 뭐든 통과
infer = 타입 분석해서 정확하게 유지
직접 쓸 일은 거의 없지만 ReturnType, Awaited 같은 내장 유틸리티 타입들이 전부 infer로 만들어져 있음. 동작 원리 이해하는 게 목표.
2. Utility Types
Partial - 전부 선택으로
1
2
3
4
5
6
7
8
9
10
11
12
13
| interface User {
id: number;
name: string;
email: string;
}
type UpdateUserForm = Partial<User>;
// → { id?: number; name?: string; email?: string }
// 내부 구현
type Partial<T> = {
[K in keyof T]?: T[K];
};
|
Required - 전부 필수로
1
2
3
4
5
6
7
| type RequiredUser = Required<Partial<User>>;
// → { id: number; name: string; email: string }
// 내부 구현
type Required<T> = {
[K in keyof T]-?: T[K]; // -? 는 ? 제거
};
|
Pick - 원하는 것만 골라
1
2
3
4
5
6
7
| type UserPreview = Pick<User, "id" | "name">;
// → { id: number; name: string }
// 내부 구현
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
|
Omit - 원하는 것만 빼
1
2
| type UserWithoutEmail = Omit<User, "email">;
// → { id: number; name: string }
|
실무 활용 패턴
1
2
3
4
5
6
| type CreateUserForm = Omit<User, "id">; // id 없이 생성
type UpdateUserForm = Partial<Omit<User, "id">>; // id 없이 부분 수정
type UserPreview = Pick<User, "id" | "name">; // 일부만 표시
// User에 phone 추가되면 위 타입들 자동 반영 ✅
// ?:로 직접 만들면 전부 손으로 수정해야 함 ❌
|
3. keyof & in
keyof - 객체 타입의 키 뽑기
1
2
3
4
5
6
7
8
9
10
| type UserKeys = keyof User;
// → "id" | "name" | "email"
// 활용
function getValue(obj: User, key: keyof User) {
return obj[key];
}
getValue(user, "name"); // ✅
getValue(user, "없는키"); // ❌ 컴파일 에러
|
in - 키 하나씩 순회 (for문이랑 같은 개념)
1
2
3
4
5
6
| type Partial<T> = {
[K in keyof T]?: T[K];
// ^^^^^^^^^^
// keyof T로 키 뽑고
// in으로 하나씩 돌면서 ? 붙임
};
|
4. never
한 줄 요약: 절대 오면 안 됨. 아무것도 할당 불가
1
2
3
4
| const a: never = "string"; // ❌
const b: never = 123; // ❌
const c: never = null; // ❌
const d: never = undefined; // ❌
|
실무에서 케이스 누락 방지
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| type Status = "loading" | "success" | "error";
function handle(status: Status) {
switch (status) {
case "loading":
return "로딩중";
case "success":
return "성공";
case "error":
return "에러";
default:
const check: never = status;
// Status에 새 값 추가하고 case 빠뜨리면 여기서 에러 ✅
}
}
// "pending" 추가하고 case 안 쓰면
// → "pending을 never에 할당할 수 없음" 에러
|
void vs never
1
2
3
4
5
6
| void; // 리턴값 없음 (undefined 반환)
never; // 아예 끝까지 실행 안 됨 (에러 or 무한루프)
function throwError(msg: string): never {
throw new Error(msg); // 절대 리턴 안 함
}
|
5. Conditional Types
기본
중첩 패턴
1
2
3
4
5
6
7
8
9
10
| type TypeName<T> = T extends string
? "string"
: T extends number
? "number"
: T extends boolean
? "boolean"
: "object";
type A = TypeName<string>; // → "string"
type B = TypeName<number>; // → "number"
|
분산 조건부 타입
1
2
3
4
5
| type IsString<T> = T extends string ? true : false;
type C = IsString<string | number>;
// → true | false
// 유니온이면 하나씩 다 검사해서 합침
|
NonNullable
1
2
3
4
5
| type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null | undefined>;
// → string
// null, undefined는 never로 바꿔서 제거
|
6. Template Literal Types
한 줄 요약: 문자열 타입 조합
1
2
3
4
5
| type Direction = "left" | "right" | "top" | "bottom";
type Property = "margin" | "padding";
type CSSProperty = `${Property}-${Direction}`;
// → "margin-left" | "margin-right" | ... 8개 자동 생성
|
실무 활용
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 이벤트 핸들러 자동 생성
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`;
// → "onClick" | "onFocus" | "onBlur"
// React Query queryKey 오타 방지
type QueryKey = "user" | "product" | "order";
type QueryKeyList = `${QueryKey}List`;
// → "userList" | "productList" | "orderList"
const { data } = useQuery({
queryKey: [QueryKeyList], // 오타나면 바로 에러 ✅
});
|
7. Mapped Types 커스텀
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // 모든 값을 string으로
type Stringify<T> = {
[K in keyof T]: string;
};
// 폼 에러 타입 자동 생성
type FormErrors<T> = {
[K in keyof T]?: string;
};
interface LoginForm {
email: string;
password: string;
}
type LoginFormErrors = FormErrors<LoginForm>;
// → { email?: string; password?: string }
// LoginForm 바뀌면 자동 반영 ✅
// 읽기 전용
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
|
8. 타입 설계 패턴
Discriminated Union (제일 중요)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // ❌ 이렇게 하면 망함
interface Response {
data?: User;
error?: string;
loading?: boolean;
// data 있을 때 error도 있을 수 있음
}
// ✅ 이렇게 해야 함
type Response =
| { status: "loading" }
| { status: "success"; data: User }
| { status: "error"; error: string };
// React Query랑 활용
function UserCard(props: Response) {
if (props.status === "loading") return <Spinner />;
if (props.status === "error") return <Error msg={props.error} />;
return <div>{props.data.name}</div>; // 여기서 data는 반드시 User ✅
}
|
필수/선택 분리
1
2
3
4
5
| type RequiredFilter = Required<Pick<Filter, "page" | "limit">>;
type OptionalFilter = Partial<Pick<Filter, "search" | "status">>;
type UserFilter = RequiredFilter & OptionalFilter;
// page, limit 필수 / search, status 선택
|
타입 가드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| type Admin = { role: "admin"; permissions: string[] };
type User = { role: "user"; name: string };
type Member = Admin | User;
function isAdmin(member: Member): member is Admin {
return member.role === "admin";
}
function handle(member: Member) {
if (isAdmin(member)) {
console.log(member.permissions); // ✅ Admin으로 좁혀짐
} else {
console.log(member.name); // ✅ User로 좁혀짐
}
}
|
9. React + TypeScript
컴포넌트 타입
1
2
3
4
5
6
| interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "danger";
loading?: boolean;
children: React.ReactNode;
}
// button 기본 속성 전부 자동으로 받음 ✅
|
Generic 컴포넌트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
|
Hook 타입
1
2
3
4
5
6
7
| function useUser(id: number) {
const [user, setUser] = useState<User | null>(null);
const [error, setError] = useState<Error | null>(null);
return [user, error, setUser] as const;
// as const 없으면 타입 추론 망가짐 ❌
}
|
이벤트 타입 치트시트
1
2
3
4
| onChange → React.ChangeEvent<HTMLInputElement>
onSubmit → React.FormEvent<HTMLFormElement>
onClick → React.MouseEvent<HTMLButtonElement>
onKeyDown → React.KeyboardEvent<HTMLInputElement>
|
핵심 요약
| 개념 |
한 줄 요약 |
infer |
타입 구조 안에서 꺼내는 것 |
never |
절대 오면 안 됨 |
keyof |
객체 타입의 키 뽑기 |
in |
키 하나씩 순회 |
Partial |
전부 선택으로 |
Pick/Omit |
골라서 쓰기 |
| Conditional Types |
타입의 if-else |
| Template Literal |
문자열 타입 조합 |
| Discriminated Union |
불가능한 상태를 타입으로 막기 |