쉽게 시작하는 타입스크립트: 5- 제네릭

Bumgu·2024년 1월 25일
1

<T>,<K>,<U> 처럼 사용하며 타입스크립트에서 중복되는 코드를 효과적으로 줄이고 고급 문법을 작성할 수 있게 해주는 제네릭에 대해 알아보겠습니다.

1. 제네릭?

제네릭은 타입을 미리 정의하지 않고 사용하는 시점에 원하는 타입을 정의해서 쓸 수 있는 문법입니다. 마치 함수의 파리미터와 같은 역할을 합니다.

function getText(text) {
	return text;
}

getText()text를 받아서 그대로 반환해줍니다. 문자열이 들어오거나, 정수가 들어와도 그대로 반환합니다. 이 원리를 타입스크립트에 대입해 '타입을 넘기고 그 타입을 그대로 반환받는다' 가 제네릭의 원리입니다.

1-1. 제네릭 기본문법

function getText<T>(text: T): T {
	return text;
}

제네릭의 기본 문법입니다. 이렇게 하면 getText()를 호출할 때 아무 타입이나 넘길 수 있습니다.
getText<string>('hi') 와 같이 호출 할 수 있습니다.
이렇게 호출하면 getText()는 마치

function getText(text: string): string {
	return text;
}

라고 선언된 것과 같습니다. getText<number>(10)이렇게 호출하면 모든 타입이 number로 지정된것과 같습니다.

1-2. 제네릭을 사용하는 이유

중복되는 코드의 문제점

function getText(text: string): string {
	return text;
}
function getNumber(num: number): number {
	return num;
}
function getBool(bool: boolean): boolean {
	return bool;
}
function getArr(arr: []): [] {
	return arr;
}
function getObj(obj: {}): {} {
	return obj;
}

위의 함수들은 인자로 받아온 값을 그대로 넘겨준다는 것에 있어서 똑같은 코드입니다. 다른점은 파라미터와 반환값의 타입입니다.

이러한 코드들을 DRY(Don't Repeat Yourself) 라는 개발 원칙이 있습니다.
이런 상황에 제네릭을 사용하면

function getSomething<T>(something: T): T {
	return something;
}

반복되는 5개의 함수를 하나로 줄였습니다.

any 사용하면 되지 않을까?

any를 사용해도 됩니다. 하지만 any는 마치 자바스크립트처럼 모든 타입을 취급하는 대신, 타입스크립트의 코드 가이드나 에러방지 혜택을 받지 못하기에 좋은 방법은 아닙니다.

2. 인터페이스에 제네릭 사용하기

interface ProductDropdown {
	value: string;
    selected: boolean;
}

interface StockDropdown {
	value: number;
    selected: boolean;
}

상품목록과 생품재고를 보여주는 드랍다운 UI를 인터페이스로 정의한 코드입니다.
여기서 value에 다른 데이터 타입을 갖는 드랍다운이 필요하면 어떻게 해야될까요?

interface AddressDropdown {
	value: { city: string; zipCode: string };
    selected: boolean;
}

이렇게 추가적으로 새로운 인터페이스를 정의해 주어야 할 것입니다.

이런식으로 모든 데이터 타입을 하나하나 정의 해주면 타입 코드가 많아져 관리도 어렵고 번거로운 작업이 됩니다. 이때 제네릭을 사용할 수 있습니다.

interface Dropdown<T> {
	value: T;
    selected: boolean;
}

인터페이스 이름 오른쪽에 <T>를 붙이고, 인터페이스 속성 중 제네릭으로 받은 타입을 사용할 곳에 T타입을 연결해줍니다. 이렇게 하면 타입을 유연하게 확장이 가능하고,
위에 나왔던 ProductDropdown, StockDropdown, AddressDropdown 인터페이스를 하나의 인터페이스로 정의가 가능합니다.

  • 제네릭 사용하지 않을 시
let product: ProductDropdown;
let stock: StockDropdown;
let address: AddressDropdown;
  • 제네릭 사용 시
let product: Dropdown<string>;
let stock: Dropdown<number>;
let address: Dropdown<{ city: string; zipCode: string }>;

이렇게 Dropdown인터페이스를 하나만 만들고 제네릭을 사용해 유연하게 타입을 처리할 수 있습니다.

3. 제네릭의 타입 제약

3-1. extends 타입 제약

function getSomething<T>(something: T) : T {
	return something;
}

이 함수는 제네릭을 선언했기 때문에 어떤 타입이든 받을 수 있습니다.
만약 문자열만 받는 제네릭을 선언하고 싶다면 extends를 사용해 제약할 수 있습니다.

function getSomething<T extends string>(something: T) : T {
	return something;
}

<T>extends 타입을 적어주면 그 타입만 사용할 수 있습니다.

일반적으로 타입을 제약할 때는 여러개의 타입중 몇개만 쓸 수 있게 제약합니다.

function lengthOnly<T extends { length: number }>(value: T) {
	return value.length;
}

이렇게 하면 length속성을 갖는 타입만 취급하도록 제약합니다. 이렇게 된다면 받을 수 있는 타입은 string, array,object입니다. 이 세개의 타입이 length속성을 가지고 있기 때문입니다.

3-2. keyof

keyof는 특정 타입의 키 값을 추출해서 문자열 유니언 타입으로 변환해줍니다.

Developer타입에 마우스 커서를 올려보면 객체의 키가 유니언 타입으로 변환되어 있는걸 알 수 있습니다. 만약 속성이 3개라면 "name" | "skill" | "속성3" 형태의 유니언 타입이 반환되었을 겁니다.

function printKeys<T extends keyof { name: string; skill: string; }> (value: T) {
	console.log(value);
}

위 코드는 객체의 키 값만 인자로 받아 출력하도록 제네릭의 타입제약을 걸어놓은 함수입니다.
extendskeyof를 사용해 name, skill속성을 갖는 객체의 키만 타입으로 받겠다고 정의했습니다. 이 함수의 제네릭은 파라미터인 value 에 연결 되어있기 때문에 함수를 호출할 때 넘길 수 있는 인자는 문자열 nameskill입니다. 다른 값을 넘기면 에러가 발생합니다.


이처럼 문자열 name, skill이 아닌 겨웅에는 에러가 발생합니다.

이처럼 extendskeyof를 사용해 타입의 제약을 까다롭게 만들 수 있습니다.


타입스크립트의 제네릭에 대해서 정리해봤습니다.
불필요한 타입 코드를 줄이고, 타입스크립트의 이점인 코드 가이드도 활용할 수 있습니다.
extendskeyof내용은 헷갈리지만 제네릭은 타입스크립트의 중요한 개념이니 까먹지 않도록 하겠습니다.

profile
Software VS Me

0개의 댓글