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

✨ 제네릭 타입 별칭

type Nullish<T> = T | null | undefined;

제네릭 판별된 유니언

예시로 데이터의 성공적인 결과 또는 오류로 인한 실패를 나타내는 제네릭 결과 타입을 만들 수 있습니다.

type Result<Data> = FailureResult | SuccessResult<Data>

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

interface SuccessResult<Data> {
    data: Data;
    ok: true
}

function handleResult(result: Result<string>) {
    if(result.ok){
        console.log(result.data)
    }
    if(!result.ok){
        console.log(result.error)
    }
}

✨ 제네릭 제한자

제네릭 기본값

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

const quote1: Quote<number> = {value: 123}
const quote2: Quote = {value: "ABC"}
interface KeyValuePair<Key, Value = Key> {
    key: Key;
    value: Value;
}

const pair1: KeyValuePair<string, number> = {
    key: "ABC",
    value: 123
}

const pair2: KeyValuePair<string> = {
    key: "ABC",
    value: "123"
}

함수의 인스턴스와 마찬가지로 기본값을 설정한 제네릭은 제일 마지막에 와야합니다.

interface KeyValuePair<Value = string, Key> {
    // ERROR Required type parameters may not follow optional type parameters.
    key: Key;
    value: Value;
}

✨ 제한된 제네릭 타입

interface WithLength {
    length: number
}

function logLength<T extends WithLength>(value: T) {
    console.log(value.length)
}

keyof와 제한된 타입 매개변수

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

const person = {
    name: "ABCD",
    age: 12
}
const res = get(person, "name") // const res: string
// 정확히 string 타입으로 유추한다.

만약 위와 달리, 다음과 같이 사용한다면, 모든 속성에 대한 유니언 값을 나타냅니다.

function get<T>(obj: T, key: keyof T) {
    return obj[key]
}

const person = {
    name: "ABCD",
    age: 20
}
const res = get(person, "name") // const res: string | number
// string 타입으로 내로잉하지 못한다.

✨ Promise

대략적인 Promise의 타입 모습입니다.

type Resolve<Value> = (value: Value) => void;
type Reject = (reason: unknown) => void;
type Executor<Value> = (resolve: Resolve<Value>, reject: Reject) => void;

class PromiseLike<T> {
    constructor(executor: Executor<T>) {
        // ...
    }
}

명시적인 타입 인수가 없다면 기본적으로 unknown으로 간주합니다.

const resolve1 = new Promise((resolve) => {
    setTimeout(() => resolve("Done"), 1000)
}) // const resolve1: Promise<unknown>

const resolve2 = new Promise<string>((resolve) => {
    setTimeout(() => resolve("Done"), 1000)
}) // const resolve2: Promise<string>

.then 메서드는 반환되는 Promise의 resolve된 값을 나타내는 새로운 타입 매개변수를 받습니다.

const resolve = new Promise<string>((resolve) => {
    setTimeout(() => resolve("Done"), 1000)
}) // const resolve: Promise<string>

const res = resolve.then(text => text.length)
// const res: Promise<number>

async 함수

async 함수는 Promise를 반환합니다. Thenable(.then() 메서드가 있는 객체)가 아닌 경우, Promise,resolve가 호출된 것 처럼 래핑(Wrapping)됩니다.

async 함수는 항상 Promise 타입을 반환합니다.

async function lengthAfterSecond(text: string) {
    await new Promise((resolve) => setTimeout(resolve, 1000))
    return text.length
} // function lengthAfterSecond(text: string): Promise<number>

async function lengthImmediately(text: string) {
    return text.length
} // function lengthImmediately(text: string): Promise<number>

async function giveString() {
    return "String!"
} // function giveString(): Promise<string>

✨ 제네릭 올바르게 사용하기

제네릭 황금률

함수에 타입 매개변수가 필요한지 여부를 판단할 수 있는 간단한 방법은, 타입 매개변수가 최소 두 번 이상 사용되었는지 확인하는 것입니다.

function logValue<Input extends string>(input: Input){
    console.log(input)
}

위 Input 타입을 선언하는 것은 쓸모없습니다. 다음과 같이 수정합시다.

function logValue(input: string){
    console.log(input)
}

제네릭 명명 규칙

기본적으로 첫 번째 타입 인수는 T를 사용하고, 다음으로 U,V 등을 사용합니다.

타입 인수가 어떻게 사용되어야 할지, 맥락과 정보가 알려진 경우라면, 해당 용어의 첫 글자를 사용하는 것으로 확장합니다.

제네릭의 의도가 명확하지 않다면, 타입 이름을 사용하세요.

function labelBoxFn1<L,V>(l: L, v:V) { } // 불분명합니다.
function labelBoxFn2<Lable,Value>(lable: Lable, v:Value) { } // OK