러닝 타입스크립트 | ch 10. 제네릭

doodoo·2023년 3월 16일
1
post-thumbnail

제네릭

  • 제네릭을 사용하면 호출하는 방식에 따라 다양한 타입으로 작동하게 만들 수 있다.
  • 타입 매개변수는 전형적으로 T나 U같은 단일 문자 이름 또는 Key, Value 같은 파스칼 케이스로 작명한다.
  • 문법: 이름<타입매개변수>
    • ex) someFunction<T>, SomeInterface<T>


1. 제네릭 함수

제네릭 함수

함수를 제네릭으로 만들면 해당 타입 매개변수를 함수 본문 내부의 매개변수 타입 에너테이션, 반환 타입 애너테이션, 타입 에너테이션에서 사용할 수 있다.

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

// 화살표 함수 
const identity = <T>(input: T) => input;

❗️제네릭 화살표 함수 구문은 .tsx 파일에서 JSX 구문과 충돌할 수 있으므로 설정을 바꿔야 한다.


명시적 제네릭 호출 타입

  • 제네릭 함수 호출 시 타입스크립트는 함수가 호출되는 방식에 따라 타입 인수를 유추한다.
  • 기본값이 unknown으로 설정되는 것을 피하기 위해 해당 타입 인수가 무엇인지 명시적으로 알려주는 명시적 제네릭 타입 인수를 사용해 함수를 호출할 수 있다.
  • 타입스크립트는 매개변수가 타입 인수로 제공된 것과 일치하는지 확인하기 위해 제네릭 호출에서 타입 검사를 수행한다.
function logWrapper<Input>(callback: (input: Input) => void) {
    return (input: Input) => {
        console.log("input", input);
        callback(input);
    };
}

// 타입: (input: string) => void
logWrapper((input: string) => {
    console.log(input.length);
});

// 타입: (input: unknown) => void
logWrapper((input) => {
    console.log(input.length); // Error: 'input' is of type 'unknown'.
});

// Input 제네릭을 위한 명시적 string 작성 
// 타입스크립트는 제네릭 타입 Input의 콜백 input 매개변수가 string 타입으로 해석된다고 유추함 
logWrapper<string>((input) => {
    console.log(input.length);
});

다중 함수 타입 매개변수

타입 매개변수가 여러개인 경우 쉼표로 구분해 함수를 정의한다.

// makeTuple은 두 개의 타입 매개변수를 선언하고 입력된 값을 읽기 전용 튜플로 반환한다. 
function makeTuple<First, Second>(first: First, second: Second) {
    return [first, second] as const; // 읽기 전용 튜플로 반환 
}

// 타입: readonly [boolean, string]
let tuple = makeTuple(true, "abc"); 

함수가 여러 개의 타입 매개변수를 선언하면 함수를 호출 시 명시적으로 제네릭 타입을 모두 선언하거나 선언하지 않아야 한다. 타입스크립트는 제네릭 호출 중 일부 타입만 유추하지 못한다.

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

// OK: 타입 인수가 둘 다 제공되지 않음 
makeTuple(true, "abc"); 

// OK: 두 개의 타입 인수가 제공됨 
makeTuple<boolean, string>(true, "abc"); 
makeTuple<true, "abc">(true, "abc"); 

// Error: Expected 2 type arguments, but got 1.
makeTuple<string>(true, "abc"); 


2. 제네릭 인터페이스

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

interface Box<T> {
    inside: T;
}

let stringBox: Box<string> = {
    inside: "abc"
}

let numberBox: Box<number> = {
    inside: 123
}

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

타입스크립트는 제네릭 타입을 취하는 것으로 선언된 위치에 제공된 값의 타입에서 타입 인수를 유추한다.

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

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

// 유추된 Value 타입 인수: Date
let lastDate = getLast({
    value: new Date("09-13-1993"),
});

// 유추된 Value 타입 인수: string
let lastFruit = getLast({ 
    next: { value: "banana" }, 
    value: "apple", 
});

// 유추된 Value 타입 인수: number
let lastMismatch = getLast({ 
    next: { value: 123 }, 
    value: false, // Error: Type 'boolean' is not assignable to type 'number'.
});

인터페이스가 타입 매개변수를 선언하는 경우, 해당 인터페이스를 참조하는 모든 타입 애너테이션은 이에 상응하는 타입 인수를 제공해야 한다.

interface Add<T> {
    contents: T;
}

let missing: Add = {  // Error: Generic type 'Add<T>' requires 1 type argument(s).
    // code...
    contents: "??"
}


3. 제네릭 클래스

클래스의 각 인스턴스는 타입 매개변수로 각자 다른 타입 인수 집합을 가진다.

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

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

    getValue(key: Key): Value | undefined { 
        return this.key === key ? this.value : undefined;
    } 
}

const storage = new Secret(12345, "luggage"); // 타입: Secret<number, string>

storage.getValue(1987); // 타입: string | undefined

명시적 제네릭 클래스 타입

  • 함수 생성자에 전달된 매개변수의 타입으로부터 타입 인수를 유추할 수 있다면 타입스크립트는 유추된 타입을 사용한다.
  • 하지만 유추할 수 없는 경우 기본값은 unknown이 된다.
  • 명시적 타입 인수를 제공하면 기본값이 unknown이 되는 것을 피할 수 있다.
class CurriedCallback<Input> { 
    #callback: (input: Input) => void;

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

    call(input: Input) {
        this.#callback(input);
    }
}

// CurriedCallback의 Input 타입 인수를 string으로 명시적으로 제공 
// 타입: CurriedCallback<string> 
new CurriedCallback((input: string) => {
    console.log(input.length);
});

// 타입: CurriedCallback<unknown> 
new CurriedCallback((input) => {
    console.log(input.length);
    // Error: Property 'length' does not exist on type 'unknown'.
});

제네릭 클래스 확장

클래스를 확장하는 경우, extends 키워드 다음에 오는 기본 클래스에 타입 인수를 제공한다.

class Quote<T> {
    lines: T;

    constructor(lines: T) { 
        this.lines = lines;
    } 
}

// SpokenQuote 클래스는 기본 클래스 Quote<T>에 대한 T 타입 인수로 string을 제공 
class SpokenQuote extends Quote<string[]> {
    speak() {
        console.log(this.lines.join("\n"));
    } 
}

new Quote("The only real failure is the failure to try.").lines; // 타입: string 
new Quote([4, 8, 15, 16, 23, 42]).lines; // 타입: number[]

new SpokenQuote([ "Greed is so destructive.", "It destroys everything", ]).lines; // 타입: string[]
new SpokenQuote([4, 8, 15, 16, 23, 42]);
// Error: Argument of type 'number' is not assignable to parameter of type 'string'.

정적 클래스 제네릭

  • 클래스의 정적 멤버는 클래스의 특정 인스턴스와 연결되어 있지 않다.
  • 정적 클래스 메서드는 자체 타입 매개변수를 선언할 수 있지만, 클래스에 선언된 어떤 타입 매개변수에도 접근할 수 없다.
class BothLogger<OnInstance> {
    instanceLog(value: OnInstance) { 
        console.log(value);
        return value;
    }

    static staticLog<OnStatic>(value: OnStatic) {
        let fromInstance: OnInstance;
        // Error: Static members cannot reference class type arguments.
        
        console.log(value);
        return value;
    } 
}


4. 제네릭 타입 별칭

제네릭 판별된 유니언

  • 타입 별칭에도 제네릭을 사용할 수 있다.
  • 제네릭 타입과 판별된 타입을 함께 사용해서 타입을 좁힐 수 있다.
// 데이터의 성공 결과, 실패 결과를 나타내는 제네릭 '결과' 타입을 만들기 위해 타입 인수를 추가했다. 

// succeeded 판별자는 성공/실패 여부에 대한 결과를 좁히는 데 사용한다. 
type Result<Data> = FailureResult | SuccessfulResult<Data>;

interface FailureResult { 
    error: Error;
    succeeded: false;
}

interface SuccessfulResult<Data> {
    data: Data;
    succeeded: true;
}

function handleResult(result: Result<string>) {
    if (result.succeeded) {
        // result: SuccessfulResult<string>의 타입 
        console.log('We did it! ${result.data}');
    } else {
        // result: FailureResult의 타입
        console.error('Awww... ${result.error}');
    }
        
    result.data;
    // Error: Property 'data' does not exist on type 'Result<string>'.
    // Property 'data' does not exist on type 'FailureResult'.
}


5. 제네릭 제한자

제네릭 기본값

타입 매개변수 뒤에 = 기본타입을 작성하면 제네릭의 기본값을 설정할 수 있다.

interface Quote<T = string> {
    value: T;
}

타입 매개변수는 동일한 선언 안의 앞선 타입 매개변수를 기본값으로 가질 수 있다.

interface KeyValue<Key, Value = Key> {
    key: Key;
    value: Value;
}


6. 제한된 제네릭 타입

매개변수 이름 뒤에 extends 키워드와 함께 제한할 타입을 작성하면 타입 매개변수를 제한할 수 있다.

interface WithLength { 
    length: number;
}

function logWithLength<T extends WithLength>(input: T) {
    console.log(`Length: ${input.length}`);
    return input;
}

logWithLength("Hello");
logWithLength([1, 2, 3]);
logWithLength({length: 123});
logWithLength(new Date()); // Error

keyof와 제한된 타입 매개변수

extendskeyof를 함께 사용하면 타입 매개변수를 이전 타입 매개변수의 키로 제한할 수 있다. (제네릭 타입의 키를 지정하는 유일한 방법)

function get<T, Key extends keyof T>(container: T, key: Key) {
    return container[key];
}

const roles = { 
    favorite: "Fargo", 
    others: ["Almost Famous", "Burn After Reading", "Nomadland"], 
};

const favorite = get(roles, "favorite"); // 타입: string 
const others = get(roles, "others"); // 타입: string[]
const missing = get(roles, "extras");
// Error: Argument of type '"extras"' is not assignable
// to parameter of type '"favorite" | "others"'.


7. Promise

promise 생성

  • 타입스크립트에서 Promise 생성자는 단일 매개변수를 받도록 작성된다.
  • 값을 resolve하려는 Promise를 만들려면 Promise의 타입 인수를 명시적으로 선언해야 한다.
// 타입: Promise<unknown> 
const resolvesUnknown = new Promise((resolve) => { 
    setTimeout(() => resolve("Done!"), 1000);
});

// 타입: Promise<string> 
const resolvesString = new Promise<string>((resolve) => { 
    setTimeout(() => resolve("Done!"), 1000);
});

async 함수

async 함수의 반환 타입은 Promise 타입이다.

// OK: Promise<string>
async function givesPromiseForString1(): Promise<string> {
    return "Done!";
}

// OK: Promise<string>
async function givesPromiseForString2() {
    return "Done!";
}

// Error: The return type of an async function
// or method must be the global Promise<T> type.
async function givesString(): string { 
    return "Done!";
}


8. 제네릭 올바르게 사용하기

제네릭 명명 규칙

타입 매개변수에 대한 표준 명명 규칙

  • 첫 번째 타입 인수로 T를 사용한다.
  • 후속 타입 매개변수가 존재하면 U, V 등을 사용한다.
  • 제네릭의 의도가 단일 문자 T에서 명확하지 않은 경우, 타입의 사용 용도를 나타내는 이름을 작성하는게 좋다.
// L과 V가 어떤 의미인지 알 수 없다. 
function lableBox<L, V>(l: L, v: V) { }

// 좀 더 명확하다. 
function lableBox<Label, Value>(label: Label, value: Value) { }

0개의 댓글