(SEB_FE) Section4 Unit5 TypeScript의 제네릭(Generic)

PYM·2023년 5월 31일
0

(SEB_FE) SECTION4

목록 보기
19/24
post-thumbnail

제네릭(Generic)은 코드 재사용성을 높이고 타입 안정성을 보장하는 기능이다.

제네릭을 사용하면 함수나 클래스를 작성할 때, 사용될 데이터의 타입을 미리 지정하지 않고, 이후에 함수나 클래스를 호출할 때 인자로 전달된 데이터의 타입에 따라 자동으로 타입을 추론하게 된다.

💚제네릭의 필요성

🦖특정 타입 부여하기

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

printLog('hello'); // 정상
printLog(123); //에러
  • printLog 함수에 특정 타입을 주어 작성한 코드로, 타입은 명시되었지만, string 타입 외에 다른 타입이 들어온다면 컴파일 에러가 난다.

🦖중복으로 함수 선언하기

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

function printLogNumber(text: number): number {
	return text;
}

printLog('hello'); // 정상
printLogNumber(123); //정상
  • 그러나 이 방법은 타입을 다르게 받기 위해 같은 코드를 타입만 바꿔서 명시하는 것이기 때문에 타입의 가독성 및 유지보수성이 나빠진다.

🦖| 연산자를 이용하기

function printLog(text: string | number) {
	return text;
}

printLog('hello'); // 정상
printLogNumber(123); //정상
  • 들어가는 인수는 해결이 되지만, 함수 내에서 결국 stringnumber가 둘 다 접근할 수 있는 API만 제공한다.
    따라서 이 외에는 타입이 정확히 추론되지 않기 때문에 사용할 수 없다.

🦖any 타입 사용하기

function printLog(text: any): any {
	return text;
}
  • 어떤 타입이든 받을 수 있지만 실제로 함수가 반환 시 타입을 추론할 수 없게 된다.

따라서 제네릭을 사용하게 될 필요성이 생긴다.

💚제네릭

제네릭을 사용해서 작성한 코드 예시를 살펴보자.

function printLog<T>(text: T): T {
	return text;
}
  • printLog 함수에 T라는 타입 변수를 추가했다.
    T는 유저가 준 파라미터의 타입을 캡처해, 이 정보를 나중에 사용할 수 있게 한다.

  • printLog 함수는 타입을 불문하고 동작하므로 제네릭이라 할 수 있다.

  • any를 쓰는 것과는 다르게 인수와 반환 타입에 string을 사용한 첫 번째 printLog 함수만큼 정확하다. 즉 타입을 추론할 수 있게 된다.

이렇게 제네릭을 작성하고 나면 아래와 같이 작성할 수 있다.

const str = printLog<string>('hello');
  • 함수를 호출할 때의 인수 중 하나로써 Tstring 타입으로 명시해 주고 타입 주변을 <>로 감싸준다.

혹은 아래와 같이 타입 추론 기능을 활용해서 작성할 수 있다.

const str = printLog('hello');
  • 전달하는 인수에 따라 컴파일러가 자동으로 T의 값을 정하는 방법.

  • 타입이 복잡해져 컴파일러가 타입을 유추할 수 없게 되는 경우엔 사용할 수 없다.

💚인터페이스와 제네릭

인터페이스에도 제네릭을 사용할 수 있다.

interface Item<T> {
	name: T;
	stock: number;
	selected: boolean;
}
  • Item 인터페이스를 사용하여 만든 객체는 name의 값으로 어떤 타입이 들어갈지만 작성을 해주면 아래와 같이 인터페이스를 여러 개 만들지 않고도 재사용을 할 수 있게 된다.
const obj: Item<string> = { 
	name: "T-shirts",
	stock: 2, 
	selected: false
};

const obj: Item<number> = { 
	name: 2044512,
	stock: 2, 
	selected: false
};

💚클래스와 제네릭

제네릭을 사용하는 TypeScript에서 팩토리를 생성할 때 생성자 함수로 클래스 타입을 참조해야 한다.

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
  • GenericNumber 클래스의 문자 그대로 사용하지만 number 타입만 쓰도록 제한하는 것은 없다. 대신 string이나 훨씬 복잡한 객체를 사용할 수 있다.

💚제네릭 타입 변수

제네릭을 사용하기 시작하면, printLog와 같은 제네릭 함수를 만들 때, 컴파일러가 함수 본문에 제네릭 타입화된 매개변수를 쓰도록 강요한다.

function printLog<T>(text: T): T {
	console.log(text.length);
	return text;
}
  • 위와 같이 console.log(text.length); 를 작성하게 되면 컴파일 에러가 난다.
    ➡ 개발자가 string 타입이 아닌 number 타입을 보낼 수도 있기 때문에, T에는 .length가 있다는 것을 추론할 수 없기 때문!

이때는 아래처럼 제네릭에 타입을 줘서 유연하게 함수의 타입을 정의해 줄 수 있다.

function printLog<T>(text: T[]): T[] {
	console.log(text.length);
	return text;
}
  • 이 제네릭 함수 코드는 일단 T라는 변수 타입을 받고, 인자 값으로는 배열 형태의 T를 받는다.
    • 따라서 제네릭 타입이 배열이기 때문에, .length를 허용하게 된다.

혹은 다음과 같이 조금 더 명시적으로 작성이 가능하다.

function printLog<T>(text: Array<T>): Array<T> {
	console.log(text.length);
	return text;
}

💚제네릭 제약 조건

앞서 제네릭 타입 변수 외에도 제네릭 함수에 어느 정도 어떤 타입이 들어올 것인지 힌트를 줄 수 있다.

function printLog<T>(text: T): T {
	console.log(text.length);
	return text;
}
  • 앞서도 살펴 봤듯이, 인자의 타입에 선언한 T는 아직 어떤 타입인지 구체적으로 정의하지 않았기 때문에 length 코드에서 오류가 난다.

  • 이럴 때 만약 해당 타입을 정의하지 않고도 length 속성 정도는 허용하려면 아래와 같이 작성하면 된다.

interface TextLength {
	length: number;
}

function printLog<T extends TextLength>(text: T): T {
	console.log(text.length);
	return text;
}

이와 같이 extends 지시자를 이용해 작성하게 되면 타입에 대한 강제는 아니지만 length에 대해 동작하는 인자만 넘겨받을 수 있게 된다.

혹은 keyof를 이용해서 제약을 줄 수도 있다.

interface Item<T> {
	name: T;
	stock: number;
	selected: boolean;
}

function printLog<T extends keyof Item>(text: T): T {
	return text;
}

printLog('name'); //정상
pirntLog('key'); //에러
  • 제네릭을 선언할 때 <T extends keyof Item> 부분에서 첫 번째 인자로 받는 객체에 없는 속성들은 접근할 수 없게끔 제한할 수 있다.
profile
목표는 "함께 일하고 싶은, 함께 일해서 좋은" Front-end 개발자

0개의 댓글