타입스크립트의 꽃, 제네릭

October 30, 2023

타입스크립트는 자바스크립트의 상위 집합 언어로, 정적 타입을 지원하여 코드의 안정성과 생산성을 향상시켜줍니다.

그 중에서도 제네릭(Generic)은 타입스크립트의 핵심 기능 중 하나입니다. 오늘은 제네릭의 개념, 사용법, 그리고 실제 활용 방법에 대해 알아봅시다!

제네릭이란?

제네릭은 타입스크립트에서 코드의 일반화(generic)를 가능하게 해주는 기능입니다. 제네릭을 사용하면 함수, 클래스, 인터페이스 등 다양한 타입에 대해 작동하도록 만들 수 있습니다.

제네릭을 사용하면

  1. 컴파일 시점에서 타입 체크를 할 수 있어 런타임 에러를 사전에 방지할 수 있고,(타입 안정성)
  2. 한 번 작성한 제네릭 코드를 다양한 타입에 대해 재사용할 수 있고,(재사용성)
  3. 제네릭을 통해 일반적인 동작을 정의하고, 구체적인 타입은 사용하는 곳에서 결정할 수 있습니다.(추상화)

그렇다면 실제로 어떻게 제네릭을 사용하는지 살펴봅시다!

1. 함수 제네릭

함수를 정의할 때, 입력값과 반환값에 대해 제네릭 타입을 지정할 수 있습니다.

const reverse = <T>(items: T[]): T[] => { return items.reverse(); }; const numbers = [1, 2, 3, 4, 5]; const reversedNumbers = reverse(numbers); // [5, 4, 3, 2, 1] const names = ['Alice', 'Bob', 'Charlie']; const reversedNames = reverse(names); // ['Charlie', 'Bob', 'Alice']

프로미스(Promise)나 콜백(callback) 함수에서도 제네릭을 유용하게 사용할 수 있습니다.

const fetchData = <T>(url: string): Promise<T> => { return fetch(url) .then((response) => response.json()) .then((data: T) => data); }; fetchData<{ id: number; name: string }>('/api/users/1') .then((user) => { console.log(user.id, user.name); }) .catch((error) => { console.log('Error:', error); })

2. 클래스 제네릭

클래스를 정의할 때, 인스턴스 변수에 대해 제네릭 타입을 지정할 수 있습니다.

class Box<T> { private item: T; constructor(item: T) { this.item = item; } getItem(): T { return this.item; } } const numberBox = new Box<number>(18); console.log(numberBox.getItem()); // 18 const stringBox = new Box<string>('Hello'); console.log(stringBox.getItem()); // 'Hello'

배열, 맵, 집합 등과 같은 컬렉션 데이터 구조에서 제네릭을 사용하여 다양한 타입의 요소를 저장하고 처리할 수 있습니다.

class Collection<T> { private items: T[] = []; addItem(item: T): void { this.items.push(item); } getItems(): T[] { return this.items; } } const numberCollection = new Collection<number>(); numberCollection.addItem(1); numberCollection.addItem('Apple'); // Error: Argument of type 'string' is not assignable to parameter of type 'number'. const stringCollection = new Collection<string>(); stringCollection.addItem('Apple'); stringCollection.addItem(1); // Error: Argument of type 'number' is not assignable to parameter of type 'string'.

3. 컴포넌트 제네릭

컴포넌트에서도 제네릭을 사용하여 타입 안정성을 확보하고 재사용성을 높일 수 있습니다.

const ListComponent = <T extends ReactNode>({ data }: { data: T[] }) => ( <ul> {data.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> ); const stringList: string[] = ['Apple', 'Banana', 'Orange']; const numberList: number[] = [1, 2, 3]; const RenderListComponents = () => { <> {/* data: string[] */} <ListComponent data={stringList} /> {/* data: number[] */} <ListComponent data={numberList} /> {/* Error: Type 'number[]' is not assignable to type 'string[]'. */} <ListComponent<string> data={numberList} /> </> }

"어라, 저 extends는 뭔가요?🙄"

제네릭 제약(Constraints)

제네릭을 제한해서 사용할 순 없을까요? 이럴 때 사용할 수 있는 것이 제네릭 제약입니다. 제네릭 제약은 extends 키워드를 사용하여 설정됩니다. T extends SomeType에서 TSomeType 타입 또는 SomeType을 상속하는 타입으로 제한됩니다.

제네릭 제약은 제네릭 타입을 특정 타입으로 제한하기 위해 사용되는 기능입니다. 제네릭을 사용하여 범용적인 코드를 작성하면서도, 특정 타입에 대해서만 동작하도록 제약을 걸 수 있습니다. 이를 통해 코드의 타입 안정성을 더욱 높일 수 있습니다.

const getProperty = <T, K extends keyof T>(obj: T, key: K): T[K] => { return obj[key]; } const person = { name: 'Alice', age: 30 }; getProperty(person, 'name'); // 'Alice' getProperty(person, 'age'); // 30 getProperty(person, 'address'); // Error: Argument of type '"address"' is not assignable to parameter of type '"name" | "age"'.

위의 예시와 같이, 제네릭 제약을 사용하여 key의 타입이 obj의 키로 제한하는 것을 볼 수 있습니다.

타입스크립트의 제네릭은 코드의 재사용성과 유연성을 높여주는 강력한 기능입니다. 제네릭을 통해 타입 안정성을 강화하고, 재사용 가능한 유연한 코드를 작성할 수 있습니다.

typescript jjang