TypeScript - Generic
제네릭이란?
- 재사용 가능한 컴포넌트(코드단위)를 만드는데 사용하며 데이터 데이터 타입을 미리 지정하지 않고 사용 시점에 데이터 타입을 설정한다
- 코드의 중복을 줄이고 타입 안정성을 보장한다. 런타임이 아닌 컴파일 시점에 타입을 체크하기 때문이다.
제네릭 기본 문법
제네릭은 변수, 함수, 클래스, 인터페이스 등 모든 컴포넌트에 적용가능하다.
function generic<T>(arg: T): T {
return arg;
}
generic<string>('hello'); // hello
generic<number>(345); // 345
- <T>는 타입 변수를 의미하며, 일반적으로 T, U, V 등 대문자 하나를 사용
- 사용 시점에 타입을 명시하거나, 인자로 전달된 값을 기준으로 자동 추론됨
여러 타입변수 사용하기
하나의 컴포넌트에서 여러 타ㅣㅂ을 다룰 수 있도록 <T, U>처럼 여러 타입 변수를 동시에 활용할 수 있다.
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
제네릭 함수+타입 단언 활용
u를 T[]로 단언함으로서 유연한 타입 캐스팅이 가능하다
단, 이런 방식은 런타임 오류 가능성도 내포하므로 신중히 사용해야 한다.
function pair2<T, U>(first: T, second: T): U {
return [first, second] as U;
}
제네릭 함수 예시
제네릭함수는 다양한 타입의 배열을 받아 동일한 로직을 수행할 수 있다.
function genericFunc<T>(args: T[]): T[] {
console.log(`배열의 길이: ${args.length}`);
return args;
}
genericFunc<boolean>([true, false, true]); // 배열의 길이: 3
제네릭 인터페이스
함수 타입의 인터페이스에도 제네릭을 적용할 수 있으며 이를 통해 다양한 타입의 함수구현을 하나의 툴로 만들 수 있다.
interface IGeneric<T> {
(key: T): T;
}
제네릭 클래스
클래스 내부에서 여러 타입을 유연하게 적용 가능하다.
class GenericClass<T> {
value: T;
add: (x: T, y: T) => T;
constructor(value: T, addFunc: (x: T, y: T) => T) {
this.value = value;
this.add = addFunc;
}
}
const myGenericNumber = new GenericClass<number>(0, (x, y) => x + y);
console.log(myGenericNumber.add(4, 6)); // 10
예제 - 배열 뒤집기
any[]):any{}로 처리하면 타입 안정성이 떨어지지만 이렇게 제네릭을 활용하여 처리하면 타입 안정성을 놓치지 않기도 하며 배열 뒤집기가 가능하다. 이때 array안에 어떤 타입이 들어오는지에 따라 타입을 추론을 해준다.
function reverseArray<T>(array: T[]): T[] {
return array.reverse();
}
reverseArray(['1', '2', '3']); // ['3', '2', '1']
reverseArray([true, false]); // [false, true]
제네릭 인터페이스
k, v 두개의 타입 변수를 활용하여 키와 값을 모두 유연하게 설정 가능하다.
interface KeyValue<K, V> {
key: K;
value: V;
}
const keyValue1: KeyValue<string, number> = {
key: 'bergerac',
value: 123
};
const keyValue2: KeyValue<boolean, string> = {
key: true,
value: 'roxan'
};
제네릭의 제약 조건(extends)
T는 반드시 length속성을 가져야 하며, 그렇지 않으면 타입 에러가 발생한다.
interface ILength {
length: number;
}
function constraints<T extends ILength>(arg: T): void {
console.log(arg.length);
}
고급기능 -keyof 연산자
객체 타입의 속성명을 문자열 리터럴 타입으로 추출한다.
type Type = { name: string; age: number };
type Union = keyof Type; // "name" | "age"
조건부 타입
T가 string 이면 'string'을 아니면 'not a string을 리턴하는 리터럴 타입
마치 타입 시스템 안에서 if-else문처럼 사용한다.
type Check<T> = T extends string ? 'String' : 'Not a String';
function checkType<T>(value: T): Check<T> {
return typeof value === 'string' ? 'String' as Check<T> : 'Not a String' as Check<T>;
}
유니온 타입과 제네릭의 결합
유니온 타입은 이전 https://bergerac.tistory.com/74 글에서 나와있듯 둘중 하나의 타입이 될 수 있는 타입을 의미한다
T extends StringOrNumber는 타입 변수 T가 문자열 또는 숫자만 받을 수 있게 제약한다.
이런 방식은 조건별 분기 처리가 가능해, 타입별 동작을 쉽게 구현할 수 있다.
type StringOrNumber = string | number;
function unionGeneric<T extends StringOrNumber>(value: T) {
if (typeof value === 'string') {
return value.toLowerCase();
}
return value;
}
unionGeneric('Hello'); // hello
unionGeneric(1000); // 1000
제네릭 유틸리티 타입
유틸리티 타입은 타입스크립트에서 기본 제공하는 제네릭 타입으로, 기존 타입을 변형하거나 재활용할때 매우 유용하다.
1. Partial<T> 일부 속성만 선택적으로
- Partial<T>는 모든 속성을 **선택적(optional)**으로 만들어 준다
- 예를 들어 Partial<User>는 { name?: string; age?: number }와 동일한 구조가 된다
- 이걸 활용하면 특정 필드만 수정하는 함수에서 타입을 간결하게 처리할 수 있다
interface User {
name: string;
age: number;
}
function updateUser(id: number, changes: Partial<User>) {
const user = users[id];
if (!user) {
console.log('해당 id의 사용자는 없습니다.');
return;
}
users[id] = { ...user, ...changes };
}
updateUser(1, { name: 'roxan' }); // name만 수정
updateUser(2, { age: 30 }); // age만 수정
2. Readonly<T> 모든 속성을 읽기 전용으로
interface Person {
name: string;
age: number;
}
const user: Readonly<Person> = {
name: 'admond',
age: 25
};
- Readonly<T>는 타입 T의 모든 속성을 읽기 전용으로 바꾼다
- 값을 수정하려고 하면 컴파일 타임에 에러 발생
- 불변성을 보장해야 할 때 유용하게 활용된다
3. Omit<T, K> 특정 속성을 제외한 타입 생성
- Omit<T, K>는 타입 T에서 속성 K를 제외한 새로운 타입을 만들어준다
- Omit<Employee, 'position'>은 position을 제외한 나머지 속성만 포함하게 된다
interface Employee {
id: number;
name: string;
age: number;
position: string;
}
type EmployeeWithoutPosition = Omit<Employee, 'position'>;
const newEmployee: EmployeeWithoutPosition = {
id: 1,
name: 'Christian',
age: 25
};
기존 타입을 기반으로 부분적인 구조를 만들고자 할 때 유용하다.