타입스크립트의 변수 - 제네릭 타입

function_dh·2024년 3월 5일
0
post-thumbnail

제네릭 타입

기존에 고정된 타입을 넘어
유니온 타입을 넘어…
함수 오버로딩을 넘어…
컴포넌트를 재사용 가능하게 구축하고 싶다면?

여러가지 타입 사용이 가능한 제네릭 타입을 사용해보자!

// 제네릭 타입으로 선언한 함수
function getText<T>(text: T): T {
  return text;
}
  • 일반적인 타입과 다르게 함수의 선언 시점이 아니라 사용 시점에 타입을 선언해줍니다.
  • T는 제네릭 타입 파라미터로, 함수가 호출될 때 실제 타입으로 대체됩니다.
  • 쉽게 제네릭 타입은 함수의 파라미터를 떠올리면 됩니다.
  • 함수의 반환값은 입력된 인자의 타입과 동일합니다.
// 함수를 호출할 때 string 타입으로 선언
getText<string>('test');

// 아래와 같은 결과를 가지게 된다!
function getText<string>(text: string): string {
  return text;
}

그래서 뭐가 좋아?

  1. 런타임 시전이 아닌 컴파일 시점에서 사전에 에러 방지가 가능합니다.
  2. 타입이 정해져 있지 않기 때문에 재사용성이 높겠죠?
  3. 개발을 할 때 IDE에서 타입을 미리 알고 있기 때문에 개발하기 편합니다.

사용시 주의 사항

타입을 배열로 받을 경우 제네릭 타입은 T[], Array<T> 로 해주어야 합니다.

나머지 타입이나 객체의 경우 따로 처리하지 않아도 됩니다.

배열 타입만 따로 선언해야 하는 이유는 엄격한 타입 체크를 위해서 입니다.

만약 T만으로 배열인지 아닌지를 구분한다면, 배열이 아닌 값도 전달할 수 있고, 그로 인해 예기치 못한 동작이 발생할 수 있기 때문입니다.

 // 배열은 length가 있기 때문에 에러가 발생하지 않음
function getArray<T>(arg: T[]): T[] {
   console.log(arg.length);
   return arg;
}

그러면 any랑 어떻게 달라?

any의 경우 함수에 어떤 값이 전달되었고 어떤 값이 반환 되었는지 알 수없지만

제네릭 타입의 경우 함수를 호출할 때 타입을 전달하기 때문에 명확히 알 수 있고 타입 추론으로 타입 체크도 가능합니다.

// 타입추론 짧은 예시 
// 컴파일러에서 전달되는 인수의 값이 Number 타입인 걸 알 수 있다.
getText(123);

interface에서의 활용

option의 타입이 일정하지 않다는 가정하에 아래와 같이 제네릭 타입을 설정하여 활용할 수 있습니다.

// option에는 제네릭 타입으로 선언되어 어떤 타입이든 들어올 수 있다
interface IExample<T> { 
   name: string;
   price: number;
   option: T;
}

const EX1: IExample<string> = {
   name: 's20',
   price: 900,
   option: 'good',
}

const EX2: IExample<{ color: string; coupon: boolean }> = {
   name: 's21',
   price: 1000,
   option: { color: 'read', coupon: false },
};

제네릭 제약 조건 with extends

위에서 설명 했듯이 어떠한 타입을 선언하든 사용이 가능하지만 반대로 타입을 제한할 수 있는 기능도 존재합니다.

일반적으로 interface에서 extends를 사용하면 타입의 확장이 이루어 지지만 제네릭 타입의 경우 extends를 사용한 타입의 종류가 제한되게 됩니다.

type numOrStr = number | string;

// 제네릭에 적용될 타입에 number | string 만 허용
function identity<T extends numOrStr>(p1: T): T {
   return p1;
}

identity(1);
identity('a');

identity(true); // 에러!!!!!
identity([]); // 에러!!!!!

속성 제약 활용

특정한 프로퍼티를 사용해야 하는 경우를 가정하면 타입의 제약 활용이 중요하다고 생각합니다.

// 아래와 같이 선언하면 에러가 발생합니다.
function getArray<T>(arg: T): T {
   console.log(arg.length);
   return arg;
}

위에 함수를 예시로 제네릭 타입에 어떤 타입도 들어갈 수 있는데 사람의 입장에서 생각하면 뭐든지 가능하니까 당연한거 아닐까? 생각 할 수 있지만

타입을 결정하는 컴파일러의 경우 타입을 전혀 알 수 없기 때문에 length라는 프로퍼티를 사용할 수가 없습니다.

function getArray<T>(arg: T): T {
	if(typeof arg === "string" || Array.isArray(arg)) {
		console.log(arg.length);
	}
	return arg;
}

타입 가드를 통해서 방지도 가능하지만 length가 아닌 직접 생성한 프로퍼티의 경우에는 사용이 불가능 합니다.

interface isOption{
   option: number;
}

function getArray<T extends isOption>(arg: T): T {
   console.log(arg.option);
   return arg;
}

getArray({ option: 10, title: 'text' });
getArray(3); // 에러 발생!

제네릭 타입은 반드시 { option: number } 프로퍼티가 포함되어야 하는 제약 조건을 쉽게 선언할 수가 있습니다.

매개변수 제약조건 활용

제네릭도 여러개 사용이 가능합니다.
일반적인 방법 말고 응용이 더 중요하겠죠?

// 전달 받은 객체의 value를 리턴해주는 함수
function getProperty<T, K extends keyof T>(obj: T, key: K) {
   return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, 'a'); // 성공
getProperty(x, 'm'); // 오류: 인수의 타입 'm' 은 'a' | 'b' | 'c' | 'd'에 해당되지 않음.

// keyof 를 통해 전달 유니온 타입으로 변환된다.
function getProperty<T, K extends 'a' | 'b' | 'c' | 'd'>(obj: T, key: K) {
   return obj[key];
}

함수 제약조건

전달 받는 파라미터가 콜백함수일 경우 제네릭 타입 설정이 가능합니다.

여기서 유추 할 수 있는게 함수 자체도 타입이 될 수 있다는 점 입니다!

function translate<T extends (a: string) => number, K extends string>(x: T, y: K): number {
  return x(y)
}

// 문자숫자를 넣으면 정수로 변환해주는 함수
const num = translate((a) => +a, '10')

console.log('num: ', num) // num : 10

제네릭 함수 타입

위에서는 전달하는 파라미터를 제어해서 제약을 주는 부분을 확인했고 파라미터 뿐만 아니라 함수 타입 구조를 설정하는 것도 가능합니다.

// 제네릭 함수 타입 구조
interface GenericIdentityFn {
  <T>(arg: T): T 
}

const identity: GenericIdentityFn = (arg) => {
  return arg
}

identity<number>(100)

함수를 할당 할때 제네릭을 결정하는 방법도 가능합니다.

interface GenericIdentityFn<T> {
  (arg: T): T

const identity: GenericIdentityFn<number> = (arg) => {
  return arg
}

identity(100)

참고문헌

profile
🍄 성장형 괴물..

0개의 댓글