kakasoo

new (…args): T 타입이란? 본문

프로그래밍/TypeScript

new (…args): T 타입이란?

카카수(kakasoo) 2023. 1. 3. 23:47
반응형
export interface Type<T = any> extends Function {
    new (...args: any[]): T;
}

 

NestJS Swagger Library에는 위와 같은 interface가 정의되어 있다.

이 타입은 신기하게도 내부에 new 키워드를 사용하고 있으며, 전개연산자와 제너릭을 모두 쓰고 있다.

이 인터페이스를 어떻게 해석하면 좋을까?

단계를 나눠서 차례대로 설명하면 아래처럼 표현할 수 있다.

 

// NOTE 1
interface MyType extends Function {
    keyName: 'kakasooFunction';
}

 

임시로 MyType이라는 interface를 만들었다.

이 인터페이스는 Function 이라는 인터페이스를 확장하여 keyName이라는 프로퍼티를 가지고 있다.

 

/**
 * Creates a new function.
 */
interface Function {
    /**
     * Calls the function, substituting the specified object for the this value of the function, and the specified array for the arguments of the function.
     * @param thisArg The object to be used as the this object.
     * @param argArray A set of arguments to be passed to the function.
     */
    apply(this: Function, thisArg: any, argArray?: any): any;

    /**
     * Calls a method of an object, substituting another object for the current object.
     * @param thisArg The object to be used as the current object.
     * @param argArray A list of arguments to be passed to the method.
     */
    call(this: Function, thisArg: any, ...argArray: any[]): any;

    /**
     * For a given function, creates a bound function that has the same body as the original function.
     * The this object of the bound function is associated with the specified object, and has the specified initial parameters.
     * @param thisArg An object to which the this keyword can refer inside the new function.
     * @param argArray A list of arguments to be passed to the new function.
     */
    bind(this: Function, thisArg: any, ...argArray: any[]): any;

    /** Returns a string representation of a function. */
    toString(): string;

    prototype: any;
    readonly length: number;

    // Non-standard extensions
    arguments: any;
    caller: Function;
}

 

따라서 일반적인 함수에 keyName이라는 키 이름으로 ‘kakasooFunction’이라는 string을 대입해주면 된다.

 

// NOTE 1
interface MyType extends Function {
    keyName: 'kakasooFunction';
}

const someFunction = () => {};
someFunction.keyName = 'kakasooFunction' as const;
const func: MyType = someFunction;

 

이렇게 작성된 코드에는 빨간색 밑줄이 나오지 않는다.

someFunction은 함수의 형태로 만들어졌으며, keyName에 정확히 일치하는 문자열이 들어갔기 때문이다.

이 첫번째 단계로, Function은 인터페이스이며, 인터페이스를 확장할 수 있고, 타입으로 쓴다는 것을 알 수 있다.

 

// NOTE 2
interface MyType2 extends Function {
    keyName: 'kakasooFunction';

    innerFunction: () => number;
}

const someFunction2 = () => {};
someFunction2.keyName = 'kakasooFunction' as const;
someFunction2.innerFunction = () => 3; // New Line!
const func2: MyType2 = someFunction2;

 

두번째 단계는, innerFunction이라는 키 이름이 추가되었고, 이 키 값은 number를 리턴하는 함수 타입이다.

따라서 keyName을 할당하던 것과 마찬가지로 함수를 하나 대입해주면 func2에도 빨간줄이 나오지 않는다.

두번째 단계에서는, 객체 내부 프로퍼티의 타입을 함수로도 정할 수 있다는 것을 알았다.

 

// NOTE 3
interface MyType3<T = any> extends Function {
    keyName: 'kakasooFunction';

    innerFunction: () => T;
}

const someFunction3 = () => {};
someFunction3.keyName = 'kakasooFunction' as const;
someFunction3.innerFunction = () => 3;
const func3: MyType3<number> = someFunction3;

 

세번째 단계에서는 제너릭 이 추가되었다.

MyType3는 any를 default type으로 하는 T generic type을 받아, 내부에서 innerFunction의 반환형으로 정의했다.

따라서 아래와 같이 MyType3<number> 라고 타입을 지정한 순간 innerFunction은 number를 리턴하게끔 강제된다.

이제 제너릭으로 받은 타입을, 확장하고자 하는 인터페이스 내부에서 쓸 수 있단 것도 알게 됐다.

 

// NOTE 4
interface MyType4<T = any> extends Function {
    keyName: 'kakasooFunction';

    (): T;
}

 

이제 인터페이스 내부에서 프로퍼티 이름이었던 innerFunction을 지워버렸다.

프로퍼티 이름으로 innerFunction이 있고, 그게 T 타입을 반환해야 한다는 의미였을 때,

 

someFunction3.innerFunction = () => 3;

 

이처럼 innerFunction에 대입한 값은 someFunction3.innerFunction() 의 형식으로 호출할 수 있었다.

이제 여기서 프로퍼티 이름이었던 innerFunction을 지운 타입을 명시했으니,

이건 someFunction4() 의 형식으로 바로 호출할 수 있어야 한다는 의미가 된다.

 

const someFunction4 = () => 3;
someFunction4.keyName = 'kakasooFunction' as const;
const func4: MyType4<number> = someFunction4;

 

따라서 이번의 func4가 가진 MyType4는 확장한 함수 인터페이스에 대한 설명이나 마찬가지다.

이제 대망의 5번째 단계다.

 

// NOTE 5
interface MyType5<T> extends Function {
    new (): T;
}

 

이 단계에서는 이름 앞에 new가 붙은, 딱 3글자만이 바뀌었다.

앞서 확장한 Function interface에 대한 설명으로, 그 자체로도 호출할 수 있어야 한다고 했는데,

이제 new가 붙은, 그 자체로도 호출할 수 있는 함수가 되었으니 이런 함수는 딱 하나 밖에 없다.

바로 클래스의 생성자 함수다.

 

// NOTE 5
interface MyType5<T> extends Function {
    new (): T;
}

class SomeClass {
    constructor() {}
}

type SomeClassConstructorType = MyType5<SomeClass>;

const SomeClassConstructor: SomeClassConstructorType = SomeClass;

 

MyType이 클래스의 생성자 함수라는 것을 알았기 때문에 이제 우리는 이러한 식들도 성립한다는 걸 알 수 있다.

먼저 MyType<SomeClass>는 SomeClass의 생성자 함수를 의미하는 함수 타입이라는 것알 알 수 있다.

그 타입을 SomeClassContructorType 이라는 직관적인 이름으로 저장해두었다.

그리고 someClasConstructor 라는 변수 명에 대한 타입으로 지정하고, SomeClass를 값으로 대입할 수 있다.

왜냐하면 생성자 함수는 곧 클래스와 동일하기 때문이다.

 

class Person {
    constructor(name: string) {}
}

 

예를 들어 위와 같이 Person 클래스가 있다고 해보자.

Person 클래스의 생성자 함수 타입은 new (name:string) ⇒ Person 이라고 정의할 수 있다.

이는 Person이라는 클래스가, new를 앞에 붙이고 이름을 받아 인스턴스를 생성하는 함수라는 점에서,

아래와 같은 표현도 가능하게 한다.

 

type PersonType = Person;
type PersonConstructorType = new (name: string) => Person;

const person: PersonType = new Person('John'); // OK
const personConstructor: PersonConstructorType = Person;
const anotherPerson: PersonType = new personConstructor('Jane'); // OK

 

다시 처음으로 돌아가, 맨 처음 이 논의의 시작이었던 Type interface를 보자.

 

export interface Type<T = any> extends Function {
    new (...args: any[]): T;
}

 

이는 Type<T = any>, 즉 어떤 클래스 T에 대한 생성자 함수를 의미한다.

이 생성자 함수는 파라미터가 몇 개인지는 아직 알 수 없고, 어쨌든 T를 생성해준다는 것만 이해할 수 있다.

 

import { Type } from '@nestjs/common';
import { ExamplesObject, ReferenceObject, RequestBodyObject, SchemaObject } from '../interfaces/open-api-spec.interface';
import { SwaggerEnumType } from '../types/swagger-enum.type';
declare type RequestBodyOptions = Omit<RequestBodyObject, 'content'>;
interface ApiBodyMetadata extends RequestBodyOptions {
    type?: Type<unknown> | Function | [Function] | string;
    isArray?: boolean;
    enum?: SwaggerEnumType;
}
interface ApiBodySchemaHost extends RequestBodyOptions {
    schema: SchemaObject | ReferenceObject;
    examples?: ExamplesObject;
}
export declare type ApiBodyOptions = ApiBodyMetadata | ApiBodySchemaHost;
export declare function ApiBody(options: ApiBodyOptions): MethodDecorator;
export {};

 

말했다시피 이건 Swagger 라이브러리 때문에 시작된 이야기였고, 여기서 Type 사용 예시를 볼 수 있다.

type은 Type 이거나 Function, [Function], string을 타입으로 가진다.

따라서 ApiBody.options.schema의 type은,

만약 type이 존재한다면 클래스 (DTO), Wrapper Function, Wrapper Function의 튜플 형태와 string,

이 중 한 가지를 타입으로 받는다는 것으로 해석될 수 있다.

반응형

'프로그래밍 > TypeScript' 카테고리의 다른 글

NTuple 타입 정의하기  (0) 2023.01.21
type Push = <T extends any[], value> = […T, value]  (0) 2023.01.20
배열의 Length를 뽑는 타입  (0) 2023.01.20
Extract<Type, Union>  (0) 2023.01.18
싱글턴(Singleton) 패턴이란?  (0) 2023.01.13