[TypeScript] 제네릭

윤지·2024년 11월 22일

TypeScript

목록 보기
10/12
post-thumbnail

1. 제네릭의 기본 개념

  • 제네릭은 타입스크립트에서 "값을 타입으로 치환"하는 역할
  • 특정 값이 여러 타입을 가질 때 활용

제네릭 사용 목적

  • 코드 재사용성 증가: 하나의 함수나 인터페이스로 다양한 타입 처리 가능
  • 타입 안정성 보장: 타입 추론 및 강제 적용으로 런타임 오류 방지
  • 가독성 및 유지보수성 향상: 중복 코드 감소

2. 제네릭의 주요 활용법

2.1 인터페이스 + 제네릭

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

// 옵션이 없는 자동차
let car: Car<null> = {
  name: "Tesla",
  color: "Red",
  option: null,
};

// 옵션이 있는 자동차
let car2: Car<{ giftcard: boolean }> = {
  name: "Tesla",
  color: "Red",
  option: { giftcard: true },
};

2.2 함수와 제네릭

타입추론 가능

기본 문법

function printValue<T>(value: T): T {
  return value;
}

printValue<string>("hello"); // "hello"
printValue<number>(10); // 10

배열의 첫 번째 요소 반환 함수

  1. 함수 선언식 + 제네릭
function getFirstElement<T>(arr: T[]): T {
  return arr[0];
}
console.log(getFirstElement<number>([1, 2, 4])); // 전송됨
  1. 함수 표현식 + 제네릭
const getFirstElement = function getFirstElement<T>(arr: T[]): T {
  return arr[0];
};
  1. 함수 표현식 + 타입 선언
const getFirstElement: <T>(arr: T[]) => T = function getFirstElement(arr) {
  return arr[0];
};
  1. 화살표 함수 + 타입 선언
const getFirstElement: <T>(arr: T[]) => T = (arr) => arr[0];

타입 정의로 함수 타입 지정

type FirstFunc = <T>(arr: T[]) => T;
const getFirstElement: FirstFunc = function getFirstElement(arr) {
  return arr[0];
};

인터페이스로 함수 타입 지정

interface FirstFunc {
  <T>(arr: T[]): T;
}
const getFirstElement: FirstFunc = (arr) => arr[0];

여러개의 타입 매개변수 사용 가능

function mergObject<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

console.log(
  mergObject<{ name: string }, { age: number }>({ name: "james" }, { age: 10 }) // 함수는 타입추론이 돼서 생략 가능
);
console.log(mergObject<{ name: string }, { gender: string }>({ name: "james" }, { gender: "male" }));

3. 심화 문법: 제네릭 제약과 고급 활용

3.1 제네릭 제약

  • 제네릭의 타입 범위를 좁혀야 할 때 제네릭 제약 사용
  • 제네릭 제약이 필요한 이유:
    • 코드의 재사용성은 높지만, 너무 광범위한 타입 허용이 문제가 될 수 있음
    • 제약은 <> 안에 추가하여 설정함
  • extends 키워드:
    • 제네릭에서 "확장" 또는 "상속"의 의미로 사용함
    • 특정 타입의 하위 타입만 허용하도록 제한함
    • 예: T extends string은 T가 string 타입이거나 string의 하위 타입이어야 함

특정 타입만 허용

function getFirstElement<T extends number | string>(arr: T[]): T {
  return arr[0];
}

console.log(getFirstElement<number>([1, 2, 4])); // 1
console.log(getFirstElement<string>(["hello", "world"])); // "hello"
// console.log(getFirstElement<boolean>([true, false])); // ❌ 오류

특정 속성을 가진 타입만 허용

// 조건: 입력값은 { length: number } 속성을 반드시 가져야 함
function getElementsLength<T extends { length: number }>(value: T): number {
  return value.length;
}

console.log(getElementsLength("hello")); // 5
console.log(getElementsLength([1, 2, 3])); // 3
// console.log(getElementsLength(10)); // ❌ 오류

여러 조건을 동시에 적용

제네릭은 여러 타입 조건을 '&'를 사용하여 동시에 적용 가능:

// T는 string과 { length: number } 둘 다 만족해야 함
function printText<T extends string & { length: number }>(text: T) {
    console.log(text, text.length);
}

3.2 객체의 속성값 추출: keyof

keyof는 객체의 키를 제네릭으로 추출할 때 사용

//getProperty 함수: 객체와 속성 키를 받아 해당 속성 값을 반환
//조건: obj는 객체여야 하고, key는 obj의 속성 중 하나여야 함
//keyof: 객체의 키를 추출해서 유니온으로 반환

function getProperty<T extends object, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

// 함수 호출: 객체 { name: "james" }에서 "name" 속성 값을 가져옴
//K extends "name"
console.log(getProperty({ name: "james", age: 20 }, "name")); // "james"
//K extends "name" | "age"
console.log(getProperty({ name: "james", age: 20 }, "age")); // 20

속성값 업데이트

function updateProperty<T extends object, K extends keyof T>(obj: T, key: K, value: T[K]): T {
  obj[key] = value;
  return obj;
}

const car = { brand: "benz", model: "s-class", year: 2020 };
console.log(updateProperty(car, "year", 2024));
// { brand: "benz", model: "s-class", year: 2024 }

4. 제네릭 활용 예제

객체 병합

두 객체를 병합하는 함수. 반환 타입은 두 객체의 타입을 합친 형태

function mergeObjects<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

console.log(mergeObjects({ name: "james" }, { age: 10 })); // { name: "james", age: 10 }

사용자 정보 출력

입력 객체가 특정 속성을 포함해야 하는 경우, 제네릭을 사용해 타입을 강제 가능

// printUserInfo 함수: Nameable과 Ageable 속성을 모두 가진 객체의 정보를 출력
// 조건: 입력 객체는 반드시 name과 age 속성을 가져야 함
type Nameable = { name: string };
type Ageable = { age: number };

// 함수 정의: Nameable & Ageable 타입을 만족하는 객체를 인자로 받음
//제네릭으로 필수, 제약 타입을 지정 가능
function printUserInfo<T extends Nameable & Ageable>(user: T): void {
  console.log(`${user.name}, ${user.age}`);
}

printUserInfo({ name: "james", age: 10 }); // james, 10

출처: 수코딩

profile
프론트엔드 공부 기록 아카이빙🍁

0개의 댓글