ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 예제로 알아보는 TypeScript 유틸리티 타입
    Javascript 2024. 6. 16. 18:15

     

     

    유틸리티 타입이 뭐지?

     

    유틸리티 타입은 TypeScript 내장 타입으로, 기존 타입에서 새로운 타입을 생성하는 데 사용됩니다.

     

    라이브러리를 만들 때 자주 사용하지만, 유틸리티 타입이 뭔지 모르는 분들도 Partial<Foo>Record<string, string> 같은 건 본 적이 있을 거예요.

     

    어떤 종류가 있는지는 문서를 참고하면 되고, 오늘은 어떤 경우에 유용한지, 몇 가지 주요 유틸리티 타입을 분류해서 소개해 드릴게요.

    TypeScript가 제공하는 타입: 만드는 것

    Record<Keys, Type>

    RecordKeys를 키로 하는 프로퍼티를 가지고, 그 프로퍼티 값의 타입은 Type인 타입을 생성합니다.

     

    주로 데이터를 저장하는 객체의 타입을 정의할 때 사용합니다.

    type FooRecord = Record<"prop1" | "prop2", string>;
    // → { prop1: string, prop2: string }

     

    위와 같은 경우에는 직접 정의하는 게 더 편할 수 있지만, 키 집합이 이미 정의되어 있는 경우 유용합니다.

    interface BarData {
        prop1: string;
        prop2: number;
        prop3: Date;
    }
    
    type BarDataErrors = Record<keyof BarData, string>
    //  → { prop1: string, prop2: string, prop3: string}

     

    string처럼 넓은 타입을 지정할 수도 있습니다.

    type StringRecord = Record<string, string>;

     

    원래 Record

    type Record<K extends keyof any, T> = {
        [P in K]: T;
    };

     

    처럼 간단하기 때문에 직접 작성해도 되지만, 의도를 명확히 표현할 수 있고 간결하게 작성할 수 있습니다.

     

    TypeScript가 제공하는 타입: 주로 프로퍼티를 가공하는 데 사용하는 것

     

    Pick<Type, Keys> / Omit<Type, Keys>

    PickType의 프로퍼티 중 Keys에 해당하는 것만 추출하여 새로운 타입을 만듭니다.

     

    OmitType의 프로퍼티 중 Keys에 해당하는 것을 제외한 나머지를 추출하여 새로운 타입을 만듭니다.

     

    즉, 화이트리스트와 블랙리스트라고 생각하면 됩니다.

     

    이러한 유틸리티 타입은 다양하게 사용되지만, 기존 함수의 래퍼를 만들 때 유용합니다.

     

    예를 들어, Button이라는 React 컴포넌트가 size라는 prop을 받는다고 할 때, size를 "large"로 고정한 LargeButton이라는 컴포넌트를 만들고 싶은 경우를 생각해 보세요.

     

    이때 LargeButton의 props 타입을 Button에서 size 프로퍼티를 제외한 것으로 만드는 것으로, Button과 동일하지만 size는 전달할 수 없다는 것을 명확히 할 수 있습니다.

    type LargeButtonProps = Omit<ButtonProps, "size">;
    
    function LargeButton(props: LargeButtonProps) {
        return <Button {...props} size="large" />
    }

    Extract<Type, Union> / Exclude<UnionType, ExcludedMembers>

     

    ExtractType에서 Union에 할당 가능한 것을 추출하여 새로운 타입을 만들고, ExcludeUnionType에서 ExcludedMembers에 할당 가능한 것을 제외한 나머지를 추출하여 새로운 타입을 만듭니다.

     

    이것은 discriminated union과 함께 사용합니다. 예를 들어, type: "success"인 경우는 정상, type: "error"인 경우는 에러인 Result 객체를 생각해 보세요.

    interface ResultSuccess {
        type: "success";
        data: Record<string, string>;
    }
    
    interface ResultError {
        type: "error";
        errorMessage: string;
    }
    
    // 기존 라이브러리가 합쳐진 타입만 export되어 있거나
    export type Result = ResultSuccess | ResultError;

     

    이러한 경우, ExtractExclude를 사용하면 제한된 타입을 얻을 수 있습니다.

    type SuccessType = Extract<Result, {type: "success"}>
    // ResultSuccess
    type NonSuccessType = Exclude<Result, {type: "success"}>
    // ResultError

    Partial<Type> / Required<Type>

     

    Partial은 프로퍼티를 선택적으로 지정 가능하게 만들고, Required는 프로퍼티를 필수로 지정하게 만듭니다.

     

    Partial은 예를 들어 프로퍼티를 부분적으로 받아서 객체를 업데이트하는 함수를 만들 때 사용할 수 있습니다.

    function updateFoo(foo: Foo, fields: Partial<Foo>) {
        return {...foo, ...fields };
    }

     

    원래 모든 프로퍼티가 required인 것은 아니기 때문에, Partial과 비교했을 때 Required를 단독으로 사용하는 경우는 적지만, 다른 유틸리티 타입과 함께 사용할 수 있습니다.

     

    예를 들어, Pick과 함께 사용하여 명확하게 사용할 수 있습니다.

    interface Foo {
        name?: string;
        age?: number;
        description?: string;
    }
    
    type Bar = Required<Pick<Foo, "name" | "age">>;

    NonNullable

     

    NonNullableType에서 nullundefined를 제거합니다.

    type NullableType = string | null;
    type NonNullType = NonNullable<NullableType>;
    // string

     

    이것은 매우 간단합니다.

     

    TypeScript가 제공하는 타입: 함수 관련

    함수 타입에 대한 유틸리티 타입은 함수를 받아서 어떤 식으로든 변경하는 함수에 필수적이지만, 특정 함수가 이미 정의되어 있는 경우에도 다음과 같은 방법으로 사용할 수 있습니다.

     

    ReturnType

    ReturnType은 함수의 타입에서 반환 값의 타입을 얻습니다.

     

    ReturnType은 함수는 알고 있지만 타입에 접근할 수 없거나 정의되지 않은 경우, 예를 들어 factory 같은 함수에서 명시적인 타입이 없는 경우, 해당 값을 중계하는 등의 작업을 위해 타입이 필요할 때 유용합니다.

    function createFoo() {
        return {
            a: "abc",
            b: 123
        };
    }
    
    type CreateFooReturn = ReturnType<typeof createFoo>;
    // { a: string, b: number }

    Parameters

     

    Parameters는 함수의 인수의 타입을 얻습니다.

     

    래퍼 함수를 만들 때, 기존 함수의 타입을 그대로 유지하면서 인수를 추가하고 싶을 때 유용합니다.

    function foo(a: string|number) {
        return a;
    }
    
    type FooParameters = Parameters<typeof foo>;
    function wrappedFoo(a: FooParameters[0], b: string) {
        other(b);
        return foo(a);
    }

     

    Awaited

    비동기 함수의 경우 return typePromise가 되지만, 그대로 사용하기는 까다롭습니다.

     

    이럴 때 Awaited를 사용하면 Promise를 풀 수 있습니다.

    interface Foo {
        a: string;
        b?: number;
    }
    
    export async function createFoo(): Promise<Foo> {
        return {
            a: "bar",
            b: 123
        };
    }
    
    ///
    
    type RequiredFoo = Required<Awaited<ReturnType<typeof createFoo>>>;
    // {a: string, b: number}

    직접 만들기

    유틸리티 타입도 TypeScript로 정의된 타입일 뿐이므로, 이러한 타입을 직접 만들 수도 있습니다.

     

    예를 들어, 부분적으로 선택적으로 지정 가능하게 만드는 PartialPartially라는 유틸리티 타입을 만들면 다음과 같습니다.

    // 이미 정의된 Foo
    interface Foo {
        name: string;
        description: string;
        startAt: Date;
        finishedAt: Date;
    }
    
    type PartialPartially<T, K extends keyof T> = Partial<Pick<T, K>> & Pick<T, keyof Omit<T, K>>;
    // 완료되지 않았을 수도 있는(finish되지 않았을 수도 있는) 타입을 만듭니다.
    type FooMayUnfinished = PartialPartial<Foo, "finishedAt">;

     

    이처럼 타입 조작 방법과 타입 조작 자체를 분리해 놓으면 나중에 봤을 때 훨씬 이해하기 쉽습니다.

    마무리

    유틸리티 타입은 사용하지 않는 것이 가장 좋지만, 어쩔 수 없는 경우에는 알고 있는지 여부에 따라 타입 처리의 난이도가 달라질 수 있습니다.

     

    유틸리티 타입을 사용하여 타입 조작을 쉽게 해 보세요.

Designed by Tistory.