
TypeScript 제네릭: any를 멈춰주세요
any를 쓰면 타입스크립트를 쓰는 의미가 없습니다. 제네릭(Generics)을 통해 유연하면서도 타입 안전성(Type Safety)을 모두 챙기는 방법을 정리합니다. infer, keyof 등 고급 기법 포함.

any를 쓰면 타입스크립트를 쓰는 의미가 없습니다. 제네릭(Generics)을 통해 유연하면서도 타입 안전성(Type Safety)을 모두 챙기는 방법을 정리합니다. infer, keyof 등 고급 기법 포함.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

프론트엔드 개발자가 알아야 할 4가지 저장소의 차이점과 보안 이슈(XSS, CSRF), 그리고 언제 무엇을 써야 하는지에 대한 명확한 기준.

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

타입스크립트를 처음 도입했을 때, 나는 완전히 잘못된 방식으로 시작했다. 모든 곳에 any를 뿌렸다. 왜냐하면 빨간 줄이 너무 많이 나왔고, 빨리 기능을 만들어야 했기 때문이다.
function getData(data: any): any {
return data;
}
function processUser(user: any) {
console.log(user.nmae); // 오타지만 에러 안 남
return user;
}
첫 주는 편했다. 빨간 줄이 다 사라졌으니까. 그런데 일주일 뒤, 프로덕션에서 앱이 터졌다. user.nmae가 undefined라는 에러였다. name을 nmae로 오타를 냈는데, any 때문에 타입스크립트가 이걸 잡아주지 못한 것이었다.
그때 이해했다. any는 타입스크립트의 보호막을 끄는 스위치라는 것을. any를 쓰면 타입스크립트를 쓰는 의미가 없다. 그냥 JavaScript를 쓰는 거랑 똑같다. 오히려 타입스크립트의 오버헤드만 더 생긴다.
하지만 현실적으로 "어떤 타입이든 받아야 하는" 상황은 있다. API 응답을 처리하는 래퍼 함수라거나, 데이터를 캐싱하는 유틸리티라거나. 이럴 때 any 없이 어떻게 하냐고?
제네릭(Generics)을 쓰면 된다. 타입 안전성을 유지하면서도 유연하게 여러 타입을 다룰 수 있다. 처음엔 어려웠지만, 이제는 제네릭 없이는 타입스크립트를 못 쓸 것 같다. 그만큼 강력하다.
내가 처음 제네릭을 본 건 React 공식 문서에서였다.
function useState<T>(initialValue: T): [T, (value: T) => void] {
// ...
}
"T가 뭔데?" 이게 내 첫 반응이었다. 꺾쇠 괄호(<>)가 낯설었고, T라는 알파벳 하나가 뭘 의미하는지 몰랐다. 처음엔 수학 공식 같아서 겁부터 났다.
그런데 코드를 계속 읽다 보니, 패턴이 보였다. useState<number>(0)이라고 쓰면 T는 number가 되는 거였다. useState<string>("")이라고 쓰면 T는 string이 되는 거였다.
결국 이거였다. 제네릭은 "타입을 파라미터처럼 넘기는 것"이었다. 함수를 호출할 때 값을 인자로 넘기듯이, 타입을 인자로 넘기는 개념. 그러니까 제네릭은 "타입을 위한 함수" 같은 거였다.
이 개념이 와닿았던 건 붕어빵 틀 비유를 봤을 때였다.
Box<T>는 붕어빵 틀이다.T는 붕어빵에 들어갈 재료다. 팥이 될 수도 있고, 슈크림이 될 수도 있다.Box<number>는 숫자가 든 상자, Box<string>은 문자열이 든 상자.틀은 하나인데, 재료를 바꿔가며 여러 붕어빵을 만드는 것. 이게 제네릭의 핵심이다.
// 제네릭 인터페이스
interface Box<T> {
item: T;
}
const numberBox: Box<number> = { item: 123 };
const stringBox: Box<string> = { item: "hello" };
const userBox: Box<User> = { item: { id: 1, name: "Ratia" } };
같은 Box 구조를 재사용하면서, 들어가는 타입만 바꾼다. any를 썼으면 item이 뭐가 들어있는지 타입스크립트가 모른다. 하지만 제네릭을 쓰면 numberBox.item은 number로 타입이 추론된다.
가장 간단한 형태는 제네릭 함수다. 들어온 값을 그대로 반환하는 identity 함수를 만들어보자.
// any를 쓴 버전 (❌ 나쁨)
function identityBad(arg: any): any {
return arg;
}
const result1 = identityBad(123); // result1의 타입: any (타입 정보 상실)
console.log(result1.toUpperCase()); // 에러 안 남! 런타임에 터짐
// 제네릭을 쓴 버전 (✅ 좋음)
function identity<T>(arg: T): T {
return arg;
}
const result2 = identity(123); // result2의 타입: number (타입 정보 유지)
console.log(result2.toUpperCase()); // 컴파일 에러! number엔 toUpperCase 없음
차이가 와닿았다. any는 타입 정보를 날려버리지만, 제네릭은 타입 정보를 보존한다. 타입 추론도 자동으로 된다. identity(123)이라고 쓰면 타입스크립트가 알아서 T를 number로 추론한다.
인터페이스에도 제네릭을 쓸 수 있다. API 응답 같은 공통 구조를 정의할 때 유용하다.
// 제네릭 인터페이스
interface ApiResponse<T> {
code: number;
message: string;
data: T; // 이 부분만 변함
timestamp: number;
}
// 사용자 정보
interface User {
id: number;
name: string;
email: string;
}
// 상품 정보
interface Product {
id: number;
title: string;
price: number;
}
// 타입 안전한 API 함수
async function fetchUser(): Promise<ApiResponse<User>> {
const response = await fetch('/api/user');
return response.json();
}
async function fetchProduct(id: number): Promise<ApiResponse<Product>> {
const response = await fetch(`/api/product/${id}`);
return response.json();
}
// 사용
const userResponse = await fetchUser();
console.log(userResponse.data.name); // ✅ 자동완성 됨!
console.log(userResponse.data.price); // ❌ 에러! User엔 price 없음
const productResponse = await fetchProduct(123);
console.log(productResponse.data.price); // ✅ 가능
이 패턴을 프로젝트에 도입하고 나서 API 응답 처리 코드가 엄청 깔끔해졌다. 응답 구조가 일정하니까 code, message, data를 매번 타이핑할 필요가 없고, data 부분만 제네릭으로 바꿔주면 됐다.
클래스에도 제네릭을 쓸 수 있다. 예를 들어 간단한 스택(Stack) 자료구조를 만들어보자.
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
size(): number {
return this.items.length;
}
}
// 숫자 스택
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.push(3);
console.log(numberStack.pop()); // 3
// 문자열 스택
const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.push("world");
console.log(stringStack.peek()); // "world"
// 커스텀 객체 스택
interface Task {
id: number;
title: string;
done: boolean;
}
const taskStack = new Stack<Task>();
taskStack.push({ id: 1, title: "Learn Generics", done: true });
taskStack.push({ id: 2, title: "Build Project", done: false });
이제 Stack은 어떤 타입이든 담을 수 있지만, 타입 안전성은 유지된다. numberStack.push("string")이라고 쓰면 컴파일 에러가 난다.
T를 제한하기제네릭이 너무 자유로우면 문제가 생긴다. "아무거나 다 받으면 안 되는데..." 하는 순간이 온다.
예를 들어, 배열이나 문자열의 길이를 출력하는 함수를 만든다고 해보자.
// 이렇게 쓰면 에러 남
function logLength<T>(arg: T): void {
console.log(arg.length); // ❌ Error! T에 length가 있다는 보장이 없음
}
타입스크립트는 T가 뭔지 모르니까, T에 length 속성이 있는지 확인할 수 없다. 이럴 때 제약조건(Constraints)을 걸어야 한다.
extends로 제약하기// T는 반드시 length 속성을 가져야 함
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): void {
console.log(arg.length); // ✅ 이제 가능
}
logLength("hello"); // OK (string은 length 있음)
logLength([1, 2, 3]); // OK (array는 length 있음)
logLength({ length: 10, value: 3 }); // OK (length 속성 있음)
logLength(10); // ❌ Error! number는 length 없음
T extends Lengthwise는 "T는 최소한 Lengthwise의 형태는 가져야 한다"는 뜻이다. 이렇게 하면 T의 자유도를 제한하면서도, 필요한 속성은 보장받을 수 있다.
제네릭 파라미터는 하나만 쓸 필요가 없다. T, U, K 등 여러 개를 동시에 쓸 수 있다.
// 두 값을 튜플로 반환
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const result1 = pair(1, "hello"); // [number, string]
const result2 = pair(true, { name: "Ratia" }); // [boolean, { name: string }]
이 패턴은 Map 같은 자료구조를 만들 때 자주 쓴다. 키와 값의 타입이 다르니까.
keyof와 제네릭 - 타입 안전한 객체 접근객체의 속성에 안전하게 접근하고 싶을 때, keyof와 제네릭을 조합하면 정말 강력하다.
// any를 쓴 버전 (❌ 나쁨)
function getPropertyBad(obj: any, key: string): any {
return obj[key]; // 오타 나도 모름
}
const user = { name: "Ratia", age: 30 };
getPropertyBad(user, "height"); // undefined 반환, 에러 안 남
"height"는 user 객체에 없는 키인데, 타입스크립트가 이걸 못 잡는다. 런타임에 undefined가 반환되고, 나중에 이걸 쓰려다가 에러가 난다.
keyof를 사용한 타입 안전 접근// 제네릭 + keyof를 쓴 버전 (✅ 좋음)
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Ratia", age: 30 };
const name = getProperty(user, "name"); // ✅ name의 타입: string
const age = getProperty(user, "age"); // ✅ age의 타입: number
const height = getProperty(user, "height"); // ❌ 컴파일 에러!
keyof T는 T 객체의 키들의 유니온 타입이다. user의 경우 keyof typeof user는 "name" | "age"가 된다. K extends keyof T라고 하면, K는 T의 키 중 하나여야 한다는 뜻이다.
그리고 반환 타입도 정확하다. T[K]는 "T 객체의 K 키에 해당하는 값의 타입"을 의미한다. user["name"]은 string이고, user["age"]는 number다. 이게 자동으로 추론된다.
이 패턴을 처음 봤을 때 정말 신기했다. 오타를 원천 봉쇄할 수 있으니까. 이제 getProperty(user, "nmae") 같은 실수를 하면 IDE가 바로 빨간 줄을 그어준다.
pick 함수객체에서 특정 키들만 골라내는 함수를 만들어보자.
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach(key => {
result[key] = obj[key];
});
return result;
}
interface User {
id: number;
name: string;
email: string;
password: string;
}
const user: User = {
id: 1,
name: "Ratia",
email: "ratia@example.com",
password: "secret123"
};
// password를 제외한 정보만 골라냄
const publicUser = pick(user, ["id", "name", "email"]);
// publicUser의 타입: { id: number; name: string; email: string; }
console.log(publicUser.name); // ✅ 가능
console.log(publicUser.password); // ❌ 에러! password는 Pick에 없음
프로덕트를 만들 때 이 패턴을 자주 쓴다. API 응답에서 비밀번호 같은 민감한 정보를 빼고 프론트엔드로 보낼 때, pick이나 omit 함수로 타입 안전하게 처리할 수 있다.
Mapped Types는 객체 타입의 모든 속성을 순회하면서 새로운 타입을 만드는 기법이다. keyof와 제네릭을 조합해서 강력한 타입 변환을 할 수 있다.
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
interface User {
name: string;
age: number;
}
type ReadonlyUser = Readonly<User>;
// 결과:
// {
// readonly name: string;
// readonly age: number;
// }
const user: ReadonlyUser = { name: "Ratia", age: 30 };
user.name = "New Name"; // ❌ 에러! 읽기 전용 속성임
[K in keyof T]는 "T의 모든 키 K를 순회하면서"라는 뜻이다. 그리고 각 키에 대해 readonly를 붙여준다.
type Partial<T> = {
[K in keyof T]?: T[K];
};
interface User {
name: string;
age: number;
email: string;
}
// 부분 업데이트 함수
function updateUser(userId: number, updates: Partial<User>) {
// DB에서 기존 유저를 가져와서 updates로 덮어씀
}
updateUser(1, { name: "New Name" }); // ✅ 일부만 전달해도 OK
updateUser(1, { age: 31, email: "new@example.com" }); // ✅ OK
실제로 CRUD 작업을 할 때 Partial은 거의 필수다. 사용자 정보를 수정할 때, 모든 필드를 다 받을 필요는 없으니까. 변경된 필드만 받아서 업데이트하면 된다.
infer - 고급 타입 추론조건부 타입(Conditional Types)은 타입 레벨에서 if-else를 하는 것이다. 그리고 infer 키워드는 "타입스크립트야, 이 부분의 타입을 네가 알아서 추론해줘"라고 부탁하는 것이다.
처음엔 이게 제일 어려웠다. 마치 타입 레벨에서 프로그래밍을 하는 느낌이었다. 하지만 이해하고 나니, 정말 강력한 도구라는 걸 깨달았다.
T extends U ? X : Ytype IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
type C = IsString<"hello">; // "yes" (literal도 string의 subtype)
"T가 string을 확장(extend)하면 'yes', 아니면 'no'"라는 뜻이다.
infer: Promise 내부 타입 꺼내기가장 자주 쓰는 예제는 Promise의 내부 타입을 추출하는 것이다.
// Promise<T>에서 T를 추출
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type A = Unwrap<Promise<string>>; // string
type B = Unwrap<Promise<number>>; // number
type C = Unwrap<string>; // string (Promise가 아니면 그대로)
// 구체적인 예제
async function fetchUser(): Promise<User> {
// ...
}
type UserType = Unwrap<ReturnType<typeof fetchUser>>; // User
infer U는 "여기 들어있는 타입이 뭔지 네가 알아내서 U라고 불러줘"라는 뜻이다. Promise<string>이 들어오면, 타입스크립트는 U를 string으로 추론한다.
처음엔 이게 마법처럼 느껴졌는데, 이제는 자주 쓴다. 특히 라이브러리 함수의 반환 타입을 추출할 때 유용하다.
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function greet(name: string, age: number): void {
console.log(`Hello, ${name}. You are ${age} years old.`);
}
type GreetParams = Parameters<typeof greet>; // [string, number]
infer P는 함수의 파라미터 타입들을 튜플로 추출한다. 이것도 타입스크립트 내장 유틸리티 타입으로 이미 있지만, 직접 만들어보면 원리를 이해할 수 있다.
type ElementType<T> = T extends (infer U)[] ? U : T;
type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
type C = ElementType<User[]>; // User
배열 타입에서 요소의 타입을 뽑아낸다. 이것도 자주 쓴다.
타입스크립트는 이미 많은 유틸리티 타입을 내장하고 있다. 이것들은 전부 제네릭으로 만들어져 있다. 각각이 어떻게 동작하는지 받아들였다.
Partial<T>: 모든 속성을 옵션으로interface User {
name: string;
age: number;
email: string;
}
type PartialUser = Partial<User>;
// {
// name?: string;
// age?: number;
// email?: string;
// }
// 사용 예: 부분 업데이트
function updateUser(id: number, updates: Partial<User>) {
// ...
}
updateUser(1, { name: "New Name" }); // ✅ age, email은 안 줘도 됨
Required<T>: 모든 속성을 필수로Partial의 반대다.
interface UserDraft {
name?: string;
age?: number;
email?: string;
}
type CompleteUser = Required<UserDraft>;
// {
// name: string;
// age: number;
// email: string;
// }
Pick<T, K>: 특정 키만 골라내기interface User {
id: number;
name: string;
email: string;
password: string;
}
type PublicUser = Pick<User, "id" | "name" | "email">;
// {
// id: number;
// name: string;
// email: string;
// }
// API 응답에서 비밀번호 제외하고 반환
function getPublicUserInfo(user: User): PublicUser {
return {
id: user.id,
name: user.name,
email: user.email
};
}
Omit<T, K>: 특정 키만 제외하기Pick의 반대다.
type UserWithoutPassword = Omit<User, "password">;
// {
// id: number;
// name: string;
// email: string;
// }
Pick이랑 결과는 같지만, 의도가 다르다. "이것만 포함"이냐, "이것만 제외"냐의 차이.
Record<K, T>: 키-값 쌍으로 객체 타입 만들기type Role = "admin" | "user" | "guest";
type Permissions = Record<Role, string[]>;
// {
// admin: string[];
// user: string[];
// guest: string[];
// }
const permissions: Permissions = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"]
};
객체의 키와 값 타입을 한 번에 정의할 수 있다. 딕셔너리 같은 구조를 만들 때 유용하다.
ReturnType<T>: 함수의 반환 타입 추출function createUser() {
return {
id: 1,
name: "Ratia",
email: "ratia@example.com"
};
}
type User = ReturnType<typeof createUser>;
// {
// id: number;
// name: string;
// email: string;
// }
함수의 반환 타입을 따로 인터페이스로 정의하지 않아도, ReturnType으로 추출할 수 있다. 특히 외부 라이브러리 함수의 반환 타입을 알고 싶을 때 유용하다.
Exclude<T, U>: 유니온 타입에서 특정 타입 제거type AllColors = "red" | "blue" | "green" | "yellow";
type PrimaryColors = Exclude<AllColors, "green" | "yellow">;
// "red" | "blue"
Extract<T, U>: 유니온 타입에서 특정 타입만 추출type AllTypes = string | number | boolean | null;
type PrimitiveNumbers = Extract<AllTypes, number>;
// number
NonNullable<T>: null과 undefined 제거type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// string
내장 유틸리티 타입만으로 부족할 때가 있다. 직접 만들어야 할 때도 있다.
DeepPartial<T>: 중첩된 객체도 전부 옵션으로type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
cache: {
enabled: boolean;
ttl: number;
};
}
type PartialConfig = DeepPartial<Config>;
const config: PartialConfig = {
database: {
host: "localhost" // port, credentials는 안 줘도 됨
}
// cache도 안 줘도 됨
};
Partial은 1단계 깊이까지만 옵션으로 만든다. 중첩된 객체의 속성까지 옵션으로 만들려면 DeepPartial이 필요하다.
NonNullableFields<T>: null/undefined 필드만 제거type NonNullableFields<T> = {
[K in keyof T]: NonNullable<T[K]>;
};
interface User {
name: string;
age: number | null;
email: string | undefined;
}
type CleanUser = NonNullableFields<User>;
// {
// name: string;
// age: number;
// email: string;
// }
Mutable<T>: readonly 제거하기type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
interface ReadonlyUser {
readonly name: string;
readonly age: number;
}
type MutableUser = Mutable<ReadonlyUser>;
// {
// name: string;
// age: number;
// }
-readonly는 "readonly를 제거하라"는 뜻이다. 마이너스 기호로 속성을 뺄 수 있다.
React를 쓰면서 제네릭의 진가를 느꼈다. 특히 커스텀 훅을 만들 때.
useQuery 훅interface QueryResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
function useQuery<T>(url: string): QueryResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
const json = await response.json();
setData(json);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [url]);
return { data, loading, error, refetch: fetchData };
}
// 사용
interface User {
id: number;
name: string;
}
function UserProfile({ userId }: { userId: number }) {
const { data, loading, error } = useQuery<User>(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!data) return null;
return <div>{data.name}</div>; // ✅ data.name은 string으로 타입 추론됨
}
제네릭 덕분에 useQuery<User>라고 쓰면, 반환된 data가 User | null 타입이 된다. 자동완성도 되고, 오타도 방지된다.
List<T>interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map(item => (
<li key={keyExtractor(item)}>
{renderItem(item)}
</li>
))}
</ul>
);
}
// 사용
interface User {
id: number;
name: string;
}
function UserList({ users }: { users: User[] }) {
return (
<List
items={users}
renderItem={user => <div>{user.name}</div>}
keyExtractor={user => user.id}
/>
);
}
List 컴포넌트는 어떤 타입의 배열이든 렌더링할 수 있다. items의 타입에 따라 renderItem과 keyExtractor의 파라미터 타입이 자동으로 추론된다.
useForm<T>interface FormState<T> {
values: T;
errors: Partial<Record<keyof T, string>>;
touched: Partial<Record<keyof T, boolean>>;
}
function useForm<T>(initialValues: T) {
const [state, setState] = useState<FormState<T>>({
values: initialValues,
errors: {},
touched: {}
});
const setValue = <K extends keyof T>(key: K, value: T[K]) => {
setState(prev => ({
...prev,
values: { ...prev.values, [key]: value }
}));
};
const setError = (key: keyof T, error: string) => {
setState(prev => ({
...prev,
errors: { ...prev.errors, [key]: error }
}));
};
const setTouched = (key: keyof T) => {
setState(prev => ({
...prev,
touched: { ...prev.touched, [key]: true }
}));
};
return {
values: state.values,
errors: state.errors,
touched: state.touched,
setValue,
setError,
setTouched
};
}
// 사용
interface LoginForm {
email: string;
password: string;
}
function LoginPage() {
const form = useForm<LoginForm>({
email: "",
password: ""
});
return (
<form>
<input
value={form.values.email}
onChange={e => form.setValue("email", e.target.value)}
onBlur={() => form.setTouched("email")}
/>
{form.errors.email && <span>{form.errors.email}</span>}
<input
type="password"
value={form.values.password}
onChange={e => form.setValue("password", e.target.value)}
onBlur={() => form.setTouched("password")}
/>
{form.errors.password && <span>{form.errors.password}</span>}
</form>
);
}
useForm<LoginForm>이라고 하면, setValue("email", value)에서 value는 자동으로 string 타입이어야 한다. setValue("age", 123) 같은 실수를 하면 컴파일 에러가 난다.
제네릭을 처음 봤을 때는 어려웠다. 꺾쇠 괄호도 낯설고, T가 뭔지 몰랐다. 하지만 이제는 제네릭 없이는 타입스크립트를 못 쓸 것 같다.
any의 안전한 대안. 유연성은 유지하면서도 타입 안전성을 보장한다.실제로 가장 자주 쓰는 패턴:
ApiResponse<T>useQuery<T>, useForm<T>pick, omit, getPropertyList<T>, Table<T>제네릭을 마스터하면, 타입스크립트의 진정한 힘을 느낄 수 있다. 처음엔 어렵지만, 계속 쓰다 보면 자연스러워진다. 그리고 일단 익숙해지면, 다시 any로 돌아갈 수 없다.
T, U, K 등을 쓴다.extends): 제네릭 타입 T가 특정 형태를 따르도록 강제하는 것.keyof Operator: 객체 타입의 모든 키를 유니온 타입으로 추출하는 연산자.T extends U ? X : Y)을 사용하는 것.infer Keyword: 조건부 타입 내에서 타입을 추론하여 변수에 할당하는 키워드.Partial, Pick, Omit 등).A | B. A 또는 B.A & B. A와 B의 속성을 모두 가짐.any와 unknown의 차이는?둘 다 모든 값을 받을 수 있지만, unknown이 더 안전하다.
any: 타입 검사를 완전히 끈다. 아무 메서드나 호출 가능. 런타임 에러 위험.
let value: any = "hello";
value.toUpperCase(); // OK
value.foo.bar.baz(); // OK (컴파일 에러 안 남, 런타임에 터짐)
unknown: 타입을 모르는 상태. 사용하기 전에 타입 체크 필요.
let value: unknown = "hello";
value.toUpperCase(); // ❌ Error! unknown은 메서드 호출 불가
if (typeof value === "string") {
value.toUpperCase(); // ✅ OK (타입 가드 후 가능)
}
원칙: 모르면 any 대신 unknown을 써라. 더 안전하다.
any의 차이는?any: 타입 정보를 완전히 잃어버린다.
function identity(arg: any): any {
return arg;
}
const result = identity(123); // result의 타입: any
제네릭: 타입 정보를 보존한다.
function identity<T>(arg: T): T {
return arg;
}
const result = identity(123); // result의 타입: number
type)의 차이는?거의 비슷하지만 몇 가지 차이가 있다:
선언 병합(Declaration Merging):
interface User {
name: string;
}
interface User {
age: number;
}
// 결과: { name: string; age: number; }
표현력:
|), 인터섹션(&), 튜플 등 더 복잡한 타입 표현 가능.
type ID = string | number;
type Point = [number, number];
확장(Extends):
extends 키워드 사용.
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
&) 사용.
type Animal = {
name: string;
};
type Dog = Animal & {
breed: string;
};
원칙: 객체 타입은 interface, 유니온이나 복잡한 타입은 type을 쓴다. 라이브러리 타이핑은 선언 병합이 필요하니 interface를 선호.
다음 상황에서 제네릭을 쓴다:
any를 쓰고 싶지만, 타입 안전성을 잃고 싶지 않을 때.infer는 언제 쓰나?주로 라이브러리나 유틸리티 타입을 만들 때 쓴다. 일반 애플리케이션 코드에서는 거의 안 쓴다. ReturnType, Parameters 같은 유틸리티 타입이 내부적으로 infer를 쓴다.
사용 예: