Programming Language/TypeScript

[TypeScript] 타입스크립트의 Generics

영벨롭 2023. 12. 27. 18:01

📌 타입스크립트 제네릭(Generics)

 변수, 함수의 매개변수 등을 정의할 때, number, string 과 같이 항상 타입을 고정하여 사용하였습니다. 하지만 이는 프로그래밍을 함에 따라 유연성과 재사용성이 떨어지게 되는 단점이 있습니다. 물론, any 나 union 타입을 통해 좀 더 유연하게 타입을 사용할 수 있지만, 이는 오류를 쉽게 찾지 못하며 가독성 또한 떨어지게 됩니다. 

 따라서 타입을 고정하여 명시하지 않고, 언제든 변할 수 있는 유연한 타입으로 표현할 수 있는 장치가 필요합니다. 이를 우리는 제네릭(generic)이라고 부릅니다. 

즉, 제네릭(generic)이란 타입을 변수화 한 것이라고 할 수 있습니다. 개발자는 제네릭을 통해 코드의 유연성과 재사용성을 높일 수 있습니다.

✅ 제네릭을 사용하는 이유

  • 타입이 고정되는 것을 방지하고, 재사용 가능한 요소를 선언할 수 있다. 
  • 타입 검사를 컴파일 시간에 진행함으로써 타입 안정성을 보장한다.
  • 캐스팅 관련 코드를 제거할 수 있다. 
  • 코드의 재사용성을 높일 수 있다. 
  • 오류를 쉽게 포착할 수 있다. 
    • any 타입을 사용하게 되면, 컴파일 시에 컴파일러가 오류를 찾지 못한다. 

📌 Generics - 함수

 우선 union 을 사용한 함수를 살펴보겠습니다. 다음과 같이 코드를 작성하게 되면, union 에 타입을 남발하게 되여 코드의 가독성이 떨어지게 됩니다. 

function getArrayLength(arr: number[] | string[] | boolean[]): number {
  return arr.length;
}

 

 이를 제네릭을 이용하여 표현하면 다음과 같습니다. 

function getArrayLength<T>(arr: T[]): number {
  return arr.length;
}

console.log(getArrayLength<number>([1, 2, 3])); // 3
console.log(getArrayLength<string>(["hello", "world"])); // 2

 

 이처럼 제네릭을 나타내는 식별자(일반적으로 T, U, V 를 사용)를 꺽쇠(<>) 안에 명시하여 함수명 뒤에 적어줍니다. 구분자는 콤마(,) 입니다. 

 함수를 호출할 때, 사용할 타입을 넘겨줄 수 있습니다. 

function getData<T>(value: T): T {
  return value;
}
const printData = <T, U>(a: T, b: U): void => {
  console.log(a, b);
};

getData<number>(1);
printData<number, string>(1, "hello");

 

 또한 제네릭 기본 값을 설정할 수 있습니다. default 타입을 지정하는 것으로, 타입을 명시하지 않고 함수 호출 시 기본 값으로 설정한 타입으로 지정됩니다. 

const makeArr = <X, Y = string>(x: X, y: Y): [X, Y] => {
  return [x, y];
};
const arr1 = makeArr<number>(4, "hello"); // [4, 'hello']
const arr2 = makeArr<number, boolean>(4, true); // [4, true]

📌 Generics - 인터페이스 & Type Alias

 제네릭은 인터페이스와 타입 별정에도 자주 쓰입니다. 

interface Vehicle<T> {
  name: string;
  color: string;
  option: T;
}

const car: Vehicle<{ price: number }> = {
  name: "Car",
  color: "red",
  option: {
    price: 1000,
  },
};
const bike: Vehicle<boolean> = {
  name: "Bike",
  color: "blue",
  option: true,
};
type Media<T> = {
  name: string;
  option: T;
}
let book: Media<boolean> = {
  name: 'Book',
  option: true
}

📌 Generics - 클래스

 클래스에서도 제네릭을 사용하여 유연하게 클래스를 다룰 타입을 지정할 수 있습니다. 

 제네릭을 클래스에서 사용할 때 주의해야할 점은, static 정적 멤버는 제네릭으로 관리할 수 없다는 점입니다. 

class Post<T> {
  id: T;
  title: string;

  constructor(id: T, title: string) {
    this.id = id;
    this.title = title;
  }
}
let post1 = new Post<number>(1, 'post1');
let post2 = new Post<string>('id2', 'post1');

 

 


📌 Generics - 제약 조건

 제네릭 사용 시, extends 키워드를 사용하여 제네릭에 적용되는 타입의 종류를 제한할 수 있습니다. 

 <T extends K> 형태의 제네릭은 T 가 K 에 할당 가능해야함을 의미합니다. 

 

 다음은 제네릭 T 에 대해 number 또는 string 타입만을 허용하는 코드입니다. 이외의 타입에 대해 함수 호출 시 에러가 발생합니다.

function func<T extends (string | number)>(a: T) {
  console.log(a);
}
func(1);
func("a");
func(true); // Error: 'boolean' 형식의 인수는 'string | number' 형식의 매개 변수에 할당될 수 없습니다.

 

✅  속성 제약조건

 예시로, .firstName 과 .lastName프로퍼티로 갖는 객체를 매개변수로 받아, 풀네임으로 반환하는 함수를 작성한다고 가정합시다.

 

 다음과 같이 작성하게 되면, T에 firstName, lastName 속성이 없다는 오류가 발생합니다. 컴파일러 입장에선 T 타입이 어떤 타입인지 모르기 때문입니다. 

// 에러
// 'T' 형식에 'firstName' 속성이 없습니다.
// 'T' 형식에 'lastName' 속성이 없습니다.
const getFullName = <T>(obj: T): string => {
  return obj.firstName + obj.lastName;
};

 

 이때 extends 키워드를 사용하여, 제네릭 타입의 프로퍼티를 반드시 해당 속성을 포함하도록 지정해주어야 합니다. 

 하나라도 포함이 안 될 경우, 오류가 발생합니다. 

const getFullName = <T extends { firstName: string; lastName: string }>(
  obj: T
): string => {
  return obj.firstName + obj.lastName;
};
let obj1 = {
  firstName: "A",
  lastName: "B",
};
let obj2 = {
  firstName: "A",
  lastName: "B",
  option: true,
};
let obj3 = {
  firstName: "A",
};
getFullName(obj1);
getFullName(obj2);
getFullName(obj3); // Error

 

✅  keyof

 keyof 연산자는 객체 형태의 타입을 속성들만 뽑아 모아 union 타입으로 만들어줍니다. 

 

 다음 코데에서 Generic U 는 Generic T 의 속성의 키 값 중 하나여야 합니다. 때문에 키 값이 아닌 'z' 를 매개 변수로 넣었을 경우 에러가 발생합니다. 

const getProperty = <T extends object, U extends keyof T>(obj: T, key: U) => {
  return obj[key];
};

getProperty({ a: 1, b: 2, c: 3 }, "a");

// Error: '"z"' 형식의 인수는 '"a" | "b" | "c"' 형식의 매개 변수에 할당될 수 없습니다.
getProperty({ a: 1, b: 2, c: 3 }, "z");
반응형