Published on

typeof와 ReturnType<T>, Parameters<T> 의 궁합을 알아보자

Authors

    🪄 typeof

    let letHello = 'hello';
    let letHelloType: typeof letHello; 
    // let letHelloType: string
    
    const constHello = 'hello';
    let constHelloType: typeof constHello; 
    // let constHelloType: 'hello'
    

    typeof를 사용할 수 있는 곳

    • typeof는 식별자(identifier)나, 그 프로퍼티에만 사용할 수 있다.

    🪄 ReturnType<Type>과 typeof

    ReturnType<Type>

    ReturnType<T>function type을 받아서, 그 return type을 반환한다.

    type Predicate = (x: unknown) => boolean;
    type K = ReturnType<Predicate>;
    // type K = boolean;
    

    ReturnType과 typeof 같이 사용하기

    ReturnType을 함수의 이름으로 사용하면, 에러가 발생한다.

    func이라는 함수는 type이 아니라, value이기 때문에 발생하는 에러이다.

    function func(){
        return {x: 10, y: 3};
    }
    
    type P = ReturnType<func>;
    // 'func' refers to a value, but is being used as a type here. 
    // Did you mean 'typeof func'?(2749)
    

    함수 func의 타입을 가져오려면, typeof 를 사용해야 한다.

    function func(){
        return {x: 10, y: 3};
    }
    
    type P = ReturnType<typeof func>;
    

    🪄 Parameters<Type>과 typeof

    Tuple type

    타입스크립트의 Tuple type

    1. Array 타입이면서
    2. 몇 개의 element가 들어가며,
    3. 그 element가 각각 어떤 타입인지도 알고 있는

    타입을 말한다.

    예를 들면, 아래와 같은 StringNumberPair는 튜플 타입이다.

    type StringNumberPair = [string, number];
    

    Parameters<Type>

    갑자기 Tuple type이 무엇인지를 얘기한 것은 Parameters<Type> 의 반환값이 Tuple type이기 때문이다.

    Parameters<Type>은 Type이라는 함수의 Type을 받고, 그 Type의 파라미터들의 타입으로 Tuple type을 구성한다.

    첫 번째 예시를 보자.

    ( ) => string 이란 함수 타입은 어떠한 파라미터도 받지 않는다.

    따라서, Parameters<() => string> 은 빈 배열의 튜플 타입을 반환하게 된다.

    type T0_1 = Parameters<() => string>;
    // type T0_1 = []
    

    Parameters와 typeof 같이 사용하기

    위의 예시처럼 함수 타입을 직접 Parameters에 지정해주는 것보다, 함수의 identifier에서 바로 그 함수의 타입을 가져오는게 편리할 수 있다.

    예를 들어 String.prototype.includes 함수의 파라미터 타입을 가져오고자 하는 상황이라고 치자.

    • case 1 ☹️ : 직접 함수 타입 입력하기
    type T1_0 = Parameters<(searchString: string, position?: number | undefined) => boolean>
    // type T1_0 = [searchString: string, position?: number | undefined]
    
    • case 2 😇 : typeof 를 써서, 바로 함수타입을 넘기기
    type T1_0 = Parameters<typeof String.prototype.includes>
    // type T1_0 = [searchString: string, position?: number | undefined]
    

    두 사례 모두, Parameters가 반환한 튜플 타입은 같다.

    다른 점이 있다면, Parameters에 함수 타입을 넣은 방식이다.

    첫 번째 케이스는 String.prototype.includes의 함수타입을 직접 입력하였다.

    두 번째 케이스는 typeof 를 사용하여 String.prototype.includes의 타입을 도출해내었다.

    첫 번째 케이스처럼 함수 타입을 직접 입력하는 것보다 typeof를 사용하는 것이 더 편리하다.

    함수 타입을 직접 입력하기엔, 함수의 타입을 정확히 알기도 어려울 뿐더러, 함수 타입을 정확히 알아내서 입력해야하는 타당한 이유는 없을 것이다.

    typeof로 바로 함수의 타입을 알아내는 것이 간편할 뿐더러, 가독성도 더 좋다.

    제네릭과 typeof + Parameters<Type>

    제네릭

    타입스크립트에서 제네릭이란, 어떠한 타입도 될 수 있는 함수를 만들어주는 도구이다.

    function genericFunction<Type>(value: Type){
        return value;
    }
    

    genericFunction이라는 함수는 Type이라는 제네릭 타입을 통해 타입을 설정할 수 있다.

    genericFunction<string>('hi');
    // function genericFunction<string>(value: string): string
    
    genericFunction<number[]>([1,2,3]);
    // function genericFunction<number[]>(value: number[]): number[]
    
    genericFunction<() => void>(() => {console.log('hi')});
    // function genericFunction<() => void>(value: () => void): () => void
    

    보시다시피, 제네릭 타입에 어떤 타입을 설정하느냐에 따라 genericFunction 함수의 함수 타입은 <string>(value: string): string이 될 수도, <number[]>(value: number[]): number[] 가 될 수도, <() => void>(value: () => void): () => void 이 될 수도 있다.

    typeof 를 사용하여 genericFunction의 함수타입을 알아낼 수 있다.

    genericFunction의 함수 타입은 <Type>(value: Type) => Type 임을 알 수 있다.

    function genericFunction<Type>(value: Type){
        return value;
    }
    type FuncType = typeof genericFunction;
    // type FuncType = <Type>(value: Type) => Type
    

    이는 곧 Parameters가 genericFunction 함수의 파라미터로 어떤 타입이 올 지 알 수 없다는 의미가 된다.

    type FuncType = typeof genericFunction;
    type UnknownTuple = Parameters<FuncType>;
    // type UnknownTuple = [value: unknown]
    

    Parameters<FuncType> 이 반환하는 타입은 genericFunction의 파라미터의 튜플 타입이다.

    반환된 타입은 genericFunction의 첫 번째 파라미터인 value의 타입을 unknown 으로 설정하였다.

    value는 제네릭 타입이라 어떠한 타입이 올지 모르므로, unknown 이라고 설정한 것이다.

    unknown

    unknown 타입에 대해 간단히 알아보면, unknown 타입은 any 의 안전한 버전이라고 할 수 있다.

    function func1(a: any) {
      a.b(); // OK
    }
    function func2(a: unknown) {
      console.log(a.key); // Object is of type 'unknown'
      console.log(a.b()); // Object is of type 'unknown'
    }
    

    any인 타입은 어떠한 짓(?)이든 할 수 있다.

    func1 함수의 파라미터인 a는 any타입이므로 b라는 메서드를 호출하고 있다.

    b라는 메서드가 없다면, a.b() 메서드를 호출하는 것은 런타임 에러가 발생할 것이다.

    반면, unknown 타입인 func2 함수의 파라미터인 a는 key라는 프로퍼티에 접근할 수도, b라는 메서드를 호출할 수도 없다.

    typeof, ReturnType<T>, Parameters<T> 활용하기 - @vue/test-utils

    import {shallowMount} from '@vue/test-utils';
    
    function getElement (wrapper: ReturnType<typeof shallowMount>, selector: string) {
      return wrapper.find(selector).element;
    }
    
    function getInputElement (wrapper: Parameters<typeof getElement>[0]) {
      const INPUT_SELECTOR = 'input';
      return getElement(wrapper, INPUT_SELECTOR) as HTMLInputElement;
    }
    
    function getLabelElement (wrapper: Parameters<typeof getElement>[0]) {
      const LABEL_SELECTOR = 'label';
      return getElement(wrapper, LABEL_SELECTOR) as HTMLLabelElement;
    }
    
    test('example test', () => {
        // given
        const id = ID;
        // when
        const wrapper = shallowMount(Component);
        // then
        expect(getInputElement(wrapper).id).toBe(id);
    });