타입스크립트 제네릭

파쏭쏭계란빡 ㅣ 2024. 3. 12. 02:18

제네릭이란

타입을 매개변수화하여 코드의 재사용성을 높이고 타입 안정성을 강화하는 기능이다.

 

사용방법

함수

함수 이름 뒤에 꺾쇠 괄호(<>) 안에 제네릭 타입 매개변수를 넣어 정의한다.

function identity<T>(arg: T): T {
    return arg;
}

const output1 = identity<string>("myString");
const output2 = identity<number>(100);

 

인터페이스

인터페이스 이름 뒤에 제네릭 타입 매개변수를 추가한다.

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

function identity<T>(arg: T): T {
    return arg;
}

//myIdentity에 identity 함수 자체를 할당한다.
let myIdentity: GenericIdentityFn<number> = identity;
console.log(myIdentity(1));

interface ResponseData<T> {
    status: number;
    payload: T;
    message: string;
}

const successResponse: ResponseData<string> = {
    status: 200,
    payload: 'ok',
    message: 'Success',
};

const userResponse: ResponseData<{ name: string; age: number }> = {
    status: 200,
    payload: { name: 'John', age: 30 },
    message: 'User fetched',
};

 

타입 별칭

타입 별칭(Type Alias) 이름 뒤에 제네릭 타입 매개변수를 추가한다.

type GenericIdentityFn<T> = { value: T };

const stringContainer: GenericIdentityFn<string> = { value: "Hello, World!" };
const numberContainer: GenericIdentityFn<number> = { value: 42 };
type LinkedList<T> = T & { next: LinkedList<T> | null };

let node3: LinkedList<number> = { value: 3, next: null };
let node2: LinkedList<number> = { value: 2, next: node3 };
let node1: LinkedList<number> = { value: 1, next: node2 };

// 문자열을 위한 LinkedList
let strNode1: LinkedList<{ value: string }> = { value: 'First', next: null };
let strNode2: LinkedList<{ value: string }> = { value: 'Second', next: strNode1 };

 

클래스

클래스 이름 뒤에 제네릭 타입 매개변수를 추가한다.

클래스의 메서드나 프로퍼티 타입으로 사용할 수 있다.

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; };
class DataStore<T> {
    private data: T[] = [];

    add(item: T) {
        this.data.push(item);
    }

    remove(item: T) {
        this.data = this.data.filter((d) => d !== item);
    }

    getAll(): T[] {
        return this.data;
    }
}

const numberStore = new DataStore<number>();
numberStore.add(1);
numberStore.add(5);
const numbers = numberStore.getAll(); // [1, 5]

const stringStore = new DataStore<string>();
stringStore.add("hello");
stringStore.add("world");
const strings = stringStore.getAll(); // ['hello', 'world']

 

주의점

  • Static과 같은 정적변수는 제네릭으로 관리할 수 없다.

 

다중 제네릭

제네릭은 한개 뿐 아니라 다중으로 사용할 수 있다,

보통 T, K, V 정도를 사용하는 것 같다.

 

함수

두 개의 서로 다른 타입 TK를 입력으로 받고, 튜플로 반환하는 함수를 정의

function pair<T, K>(x: T, y: K): [T, K] {
    return [x, y];
}

// 사용 예
const result = pair<string, number>('hello', 42);
// result의 타입은 [string, number]

 

인터페이스

interface pair<K, V> {
    key: K;
    value: V;
}

const result : pair<string, number> = { key: 'age', value: 30 };

 

클래스

class Map<K, V> {
    private items: Array<{ key: K; value: V }> = [];

    setItem(key: K, value: V): void {
        this.items.push({ key, value });
    }

    getItem(key: K): V | undefined {
        const foundItem = this.items.find(item => item.key === key);
        return foundItem ? foundItem.value : undefined;
    }
}

// 사용 예
const map = new Map<string, number>();
map.setItem('apples', 5);
map.setItem('bananas', 10);

const apples = map.getItem('apples'); // 5

 

타입을 조합한 타입

복잡한 데이터 구조에서 여러 타입 조합 가능하다.

Dictionary 타입을 정의할 때 키의 타입(K)과 값을 배열로 갖는 타입(V[])을 사용할 수 있다.

type Dictionary<K, V> = {
    [key in K]: V[];
};

// 사용 예
const dict: Dictionary<string, number> = {
    ages: [35, 42, 63],
    scores: [75, 82, 94]
};

 

extends

타입스크립트에서 extends 키워드는 주로 두 가지 경우에서 사용된다.

  1. 클래스 상속
  2. 제네릭 타입 제약

 

클래스 상속에서의 extends

클래스 상속을 할 때 extends 키워드로 기존 클래스의 모든 속성과 메서드를 상속받은 새로운 클래스를 생성할 수 있다.

(이 부분은 제네릭과 관련 없지만 extends 의 역할을 위해 추가함)

class Animal {
    move() {
        console.log("Moving along!");
    }
}

class Dog extends Animal {
    bark() {
        console.log("Woof! Woof!");
    }
}

const dog = new Dog();
dog.move(); // "Moving along!"
dog.bark(); // "Woof! Woof!"

 

제네릭 타입 제약에서의 extends

제네릭 타입 제약에서 extends를 사용하여 특정 타입이나 인터페이스를 만족하는 타입 매개변수만을 받도록 제한할 수 있다.

이는 타입 매개변수가 특정 속성이나 메서드를 갖고 있음을 보장한다.

여기서도 두가지 경우로 나뉜다.

 

1. 키가 객체에 있는지 확인하기

여기서 K extends keyof TKT의 키 중 하나임을 의미한다.

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

 

객체 T와 해당 객체에서의 키 K를 받는 함수에서, K가 T의 키인지 확인하는 제약을 추가할 수 있다

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

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

// 동작합니다: 'a'는 x의 키
const aValue = getProperty(x, "a");

// 오류: 'm'은 x의 어떤 키와도 일치하지 않음
// const mValue = getProperty(x, "m");

 

2. 두 객체 타입이 호환되는지 확인하기

두 제네릭 타입을 받아서, 하나가 다른 하나의 서브 타입인지 확인하는 함수

제네릭 제약 T extends UTU의 모든 속성을 포함해야 함

 

person 객체는 employee 객체의 모든 속성을 포함하고 있으므로, 함수 호출은 성공한다.

function extend<T extends U, U>(first: T, second: U): T {
    return first;
}

const person = { name: 'John', age: 30 };
const employee = { name: 'Jane' };

// 이 경우, 'person' 객체는 'employee' 객체의 모든 속성('name')을 가지고 있습니다.
const extendedPerson = extend(person, employee);

console.log(extendedPerson);

 

제네릭의 기본 값(Default Value)

제네릭 타입을 사용할 때, 제네릭 타입을 명시적으로 지정하지 않았을 경우 적용될 기본 타입을 정의하는 기능이다.

아래 코드에서 TempResponse 타입은 Data가 기본으로 {status: number}를 가진다.

type ApiResponse<Data = {status: number}> = {
  data: Data;
  isError: boolean;
};

//제네릭을 직접적으로 넣어주지 않으면
// TempResponse는 ApiResponse<{status: number}>와 동일하게 처리됩니다.
// 따라서 TempResponse의 data 속성은 { status: number } 타입을 가집니다.
type TempResponse = ApiResponse;

 

이렇게 직접적으로 제네릭을 지정해주지 않으면 기본으로 넣어준 { status: number } 이게 타입으로 적용된다.

type ApiResponse<Data = { status: number }> = {
    data: Data;
    isError: boolean;
};

type TempResposne = ApiResponse<{ status: boolean }>;

 

타입스크립트 제네릭