[하루 30분 러닝 타입스크립트] 제네릭 (1)

제네릭을 통해 호출하는 방식에 따라 다양한 타입으로 작동하도록 의도할 수 있습니다.

✨ 제네릭 함수

function identity<T>(input: T){
    return input
}

const identity2 = <T>(input: T) => input;

화살표 함수의 제네릭은 .tsx 파일에서 JSX 구문과 충돌합니다.

명시적 제네릭 호출 타입

function logWrapper<Input>(callback: (input: Input) => void) {
    return (input: Input) => {
        console.log("Input:", input);
        callback(input)
    }
}

logWrapper((input: string) => {
    console.log(input.length);
})

logWrapper((input) => {
    console.log(input.length) // ERROR 'input' is of type 'unknown'.
})

기본 값이 unknown으로 설정되는 것을 피하기 위해 명시적 제네릭 타입 인수를 사용할 수 있습니다.

logWrapper<string>((input) => {
    console.log(input.length)
})

다중 함수 타입 매개변수

function makeTuple<First, Second>(first: First, second: Second) {
    return [first, second] as const
}

const tuple1 = makeTuple(true,"abc") // const tuple1: readonly [boolean, string]
const tuple2 = makeTuple<any,string>(true,"abc") // const tuple2: readonly [any, string]

✨ 제네릭 인터페이스

인터페이스도 제네릭으로 선언할 수 있습니다.

interface Box<T> {
    inside : T
}

const stringBox: Box<string> = {
    inside : "ABC"
}

타입 스크립트의 내장 Array 메서드는 제네릭 인터페이스로 정의된다.

interface Array<T> {
		// ... 
    pop() : T | undefined;
    push(...items: T[]): number;
}

유추된 제네릭 인터페이스 타입

interface LinkedNode<Value> {
    next?: LinkedNode<Value>;
    value: Value;
}

function getLastNode<Value>(node: LinkedNode<Value>): Value {
    return node.next ? getLastNode(node.next) : node.value;
}

let lastDate = getLastNode({
    value: new Date()
}) // Date 타입으로 유추

let lastString = getLastNode({
    next: {
        value: 123
    }
    value: "ABC"
}) // ERROR Type 'string' is not assignable to type 'number'.

✨ 제네릭 클래스

클래스에도 제네릭을 사용할 수 있습니다.

class Secret<Key, Value>{
    key: Key;
    value: Value

    constructor(key: Key, value: Value) {
        this.key = key;
        this.value = value;
    }

    // ...
}

명시적 제네릭 클래스 타입

클래스 타입 인수를 유추할 수 없는 경우에는 타입 인수의 기본값은 unknown이 됩니다.

class CurriedCallback<Input> {
    #callback: (input: Input) => void

    constructor(callback: (input:Input) => void) {
        this.#callback = (input: Input) => {
            console.log("Input:", input)
            callback(input)
        }
    }
}

new CurriedCallback((input: string)=>{
    console.log(input.length) // OK
})

new CurriedCallback((input)=>{
    console.log(input.length) // ERROR 'input' is of type 'unknown'.
})

제네릭 클래스 확장

제네릭 클래스 역시 extends 키워드 다음에 오는 클래스로 사용할 수 있지만, 타입을 유추하진 않습니다. 명시적 타입 애너테이션을 통해 지정해야 합니다.

class Quote<T> {
    line: T;

    constructor(line: T) {
        this.line = line
    }
}

class SpokenQuote extends Quote<string[]> { // OK 제네릭을 명시해 주어야 한다.
    speak() {
        console.log(this.line.join('\\n'))
    }
}

class AttributedQuote<Value> extends Quote<Value> { // OK
    speaker: string

    constructor(value: Value, speaker: string){
        super(value);
        this.speaker = speaker;
    }
}

제네릭 인터페이스 구현

interface Interface<Type> {
    prop: Type
}

class Class implements Interface<string>{
    prop: string

    constructor(value: string) {
        this.prop = value
    }
} // OK

class Class2 implements Interface<string>{
    prop: number;
    /* ERROR 
    Property 'prop' in type 'Class2' is not assignable to the same property in base type 'Interface<string>'.
    Type 'number' is not assignable to type 'string'.
    */
}

메서드 제네릭

클래스 메서드는 인스턴스와 별개로 자체 제네릭 타입을 선언할 수 있습니다.

class Factory<Key> {
    key: Key;

    constructor(key: Key){
        this.key = key
    }

    createPair<Value>(value: Value){
        return {key: this.key, value};
    }
}

정적 클래스 제네릭

클래스의 정적 멤버는 다른 특정 인스턴스와 연결되어 있지 않습니다.

따라서 클래스에 선언된 어떤 타입 매개변수에도 접근할 수 없습니다.

class Logger<Instance> {
    static staticLog<Value>(value: Value) {
        let instance: Instance 
				// ERROR Static members cannot reference class type parameters.

        console.log(value);
        return value
    }
}