fontcolor_theme
Deepkit Runtime Types

리플렉션

Type 정보 자체를 직접 다루기 위해서는 두 가지 기본 방식이 있습니다: Type objects와 Reflection classes. Type objects는 typeOf<T>()가 반환하는 일반 JS 객체입니다. Reflection classes는 아래에서 설명합니다.

typeOf Function은 Interface, object literal, class, function, type alias를 포함한 모든 타입에서 작동합니다. 이 Function은 타입에 대한 모든 정보를 담고 있는 type object를 반환합니다. generics를 포함해 어떤 타입이든 type argument로 전달할 수 있습니다.

import { typeOf } from '@deepkit/type';

typeOf<string>(); //{kind: 5}
typeOf<number>(); //{kind: 6}

typeOf<{id: number}>(); //{kind: 4, types: [{kind: 6, name: 'id'}]}

class User {
    id: number
}

typeOf<User>(); //{kind: 4, types: [...]}

function test(id: number): string {}

typeOf<typeof test>(); //{kind: 12, parameters: [...], return: {kind: 5}}

type object는 해당 type object의 타입을 나타내는 kind Property를 포함하는 단순한 object literal입니다. kind Property는 숫자이며 ReflectionKind enum에서 그 의미를 얻습니다. ReflectionKind@deepkit/type 패키지에 다음과 같이 정의되어 있습니다:

enum ReflectionKind {
  never,    //0
  any,     //1
  unknown, //2
  void,    //3
  object,  //4
  string,  //5
  number,  //6
  boolean, //7
  symbol,  //8
  bigint,  //9
  null,    //10
  undefined, //11

  //... and even more
}

반환될 수 있는 type object는 여러 가지가 있습니다. 가장 단순한 것들은 never, any, unknown, void, null, 그리고 undefined이며, 다음과 같이 표현됩니다:

{kind: 0}; //never
{kind: 1}; //any
{kind: 2}; //unknown
{kind: 3}; //void
{kind: 10}; //null
{kind: 11}; //undefined

예를 들어 숫자 0은 ReflectionKind enum의 첫 번째 항목(이 경우 never)이고, 숫자 1은 두 번째 항목(여기서는 any)이며, 이런 식입니다. 이에 따라 string, number, boolean과 같은 primitive type은 다음과 같이 표현됩니다:

typeOf<string>(); //{kind: 5}
typeOf<number>(); //{kind: 6}
typeOf<boolean>(); //{kind: 7}

이러한 비교적 단순한 타입은 typeOf에 직접 type argument로 전달되었기 때문에 type object에 추가 정보가 없습니다. 그러나 type alias를 통해 타입이 전달된 경우에는 type object에 추가 정보를 찾을 수 있습니다.

type Title = string;

typeOf<Title>(); //{kind: 5, typeName: 'Title'}

이 경우 type alias의 이름 'Title'도 사용할 수 있습니다. type alias가 generic이면, 전달된 타입들도 type object에서 확인할 수 있습니다.

type Title<T> = T extends true ? string : number;

typeOf<Title<true>>();
{kind: 5, typeName: 'Title', typeArguments: [{kind: 7}]}

전달된 타입이 index access operator의 결과인 경우, container와 index 타입이 함께 존재합니다:

interface User {
  id: number;
  username: string;
}

typeOf<User['username']>();
{kind: 5, indexAccessOrigin: {
    container: {kind: Reflection.objectLiteral, types: [...]},
    Index: {kind: Reflection.literal, literal: 'username'}
}}

Interface와 object literal은 모두 Reflection.objectLiteral로 출력되며, types 배열에 Property와 Method를 포함합니다.

interface User {
  id: number;
  username: string;
  login(password: string): void;
}

typeOf<User>();
{
  kind: Reflection.objectLiteral,
  types: [
    {kind: Reflection.propertySignature, name: 'id', type: {kind: 6}},
    {kind: Reflection.propertySignature, name: 'username',
     type: {kind: 5}},
    {kind: Reflection.methodSignature, name: 'login', parameters: [
      {kind: Reflection.parameter, name: 'password', type: {kind: 5}}
    ], return: {kind: 3}},
  ]
}

type User  = {
  id: number;
  username: string;
  login(password: string): void;
}
typeOf<User>(); //위와 동일한 객체를 반환

index signature도 types 배열에 있습니다.

interface BagOfNumbers {
    [name: string]: number;
}


typeOf<BagOfNumbers>;
{
  kind: Reflection.objectLiteral,
  types: [
    {
      kind: Reflection.indexSignature,
      index: {kind: 5}, //string
      type: {kind: 6}, //number
    }
  ]
}

type BagOfNumbers  = {
    [name: string]: number;
}
typeOf<BagOfNumbers>(); //위와 동일한 객체를 반환

class는 object literal과 유사하며, classType(class 자체에 대한 reference)와 함께 types 배열 아래에 Property와 Method를 가집니다.

class User {
  id: number = 0;
  username: string = '';
  login(password: string): void {
     //아무 것도 하지 않음
  }
}

typeOf<User>();
{
  kind: Reflection.class,
  classType: User,
  types: [
    {kind: Reflection.property, name: 'id', type: {kind: 6}},
    {kind: Reflection.property, name: 'username',
     type: {kind: 5}},
    {kind: Reflection.method, name: 'login', parameters: [
      {kind: Reflection.parameter, name: 'password', type: {kind: 5}}
    ], return: {kind: 3}},
  ]
}

Reflection.propertySignature의 타입이 Reflection.property로, Reflection.methodSignature가 Reflection.method로 변경되었음을 주의하세요. class의 Property와 Method에는 추가 속성들이 있으므로 이 정보도 조회할 수 있습니다. 후자에는 visibility, abstract, default가 추가로 포함됩니다. class의 type object에는 super-class의 Property와 Method가 아니라 해당 class 자신만의 Property와 Method만 포함됩니다. 이는 Interface/object literal의 type object가 모든 부모의 property signature와 method signature를 types에 해석해 포함하는 것과는 반대입니다. super-class의 Property와 Method를 해석하려면 ReflectionClass와 그 ReflectionClass.getProperties()(다음 섹션 참조) 또는 @deepkit/typeresolveTypeMembers()를 사용할 수 있습니다.

type object는 정말 다양합니다. 예를 들어 literal, template literal, promise, enum, union, array, tuple 등 많은 것들이 있습니다. 어떤 것이 존재하며 어떤 정보가 제공되는지 알아보려면 @deepkit/type에서 Type을 import하는 것을 권장합니다. 이것은 TypeAny, TypeUnknonwn, TypeVoid, TypeString, TypeNumber, TypeObjectLiteral, TypeArray, TypeClass 등 모든 가능한 subtype을 포함한 union입니다. 거기에서 정확한 구조를 확인할 수 있습니다.

Type 캐시

type alias, function, class에 대해 generic argument가 전달되지 않는 한 type object는 캐시됩니다. 이는 typeOf<MyClass>() 호출이 항상 동일한 객체를 반환한다는 의미입니다.

type MyType = string;

typeOf<MyType>() === typeOf<MyType>(); //true

그러나 generic type이 사용되는 순간, 전달된 타입이 항상 동일하더라도 새로운 객체가 항상 생성됩니다. 이는 이론적으로 무한한 조합이 가능하며, 그런 캐시는 사실상 메모리 누수가 되기 때문입니다.

type MyType<T> = T;

typeOf<MyType<string>>() === typeOf<MyType<string>>();
//false

다만, 재귀 타입에서 동일한 타입이 여러 번 인스턴스화되는 순간에는 캐시됩니다. 그러나 캐시의 지속 시간은 타입이 계산되는 순간으로만 제한되며 그 이후에는 존재하지 않습니다. 또한 Type object가 캐시되더라도 새로운 reference가 반환되며 동일한 객체 그 자체는 아닙니다.

type MyType<T> = T;
type Object = {
   a: MyType<string>;
   b: MyType<string>;
};

typeOf<Object>();

MyType<string>Object가 계산되는 동안에만 캐시됩니다. 따라서 ab의 PropertySignature는 캐시에서 동일한 type을 가지지만, 동일한 Type object는 아닙니다.

모든 non-root Type object는 일반적으로 둘러싼 상위 객체를 가리키는 parent Property를 가집니다. 이는 예를 들어 어떤 Type이 union의 일부인지 아닌지 확인하는 데 유용합니다.

type ID = string | number;

typeOf<ID>();
*Ref 1* {
  kind: ReflectionKind.union,
  types: [
    {kind: ReflectionKind.string, parent: *Ref 1* } }
    {kind: ReflectionKind.number, parent: *Ref 1* }
  ]
}

'Ref 1'은 실제 union type object를 가리킵니다.

위에서 예로 든 캐시된 Type object의 경우, parent Property가 항상 실제 부모를 가리키지는 않습니다. 예를 들어 동일한 class가 여러 번 사용되는 경우, 비록 types(TypePropertySignature와 TypeMethodSignature)의 즉각적인 타입은 올바른 TypeClass를 가리키지만, 이 signature 타입들의 type은 캐시된 항목의 TypeClass의 signature 타입들을 가리킵니다. 이는 부모 구조를 무한히 읽지 말고 즉각적인 부모만 읽도록 하기 위해 알아두어야 합니다. parent가 무한 정밀도를 가지지 않는 것은 성능상의 이유입니다.

JIT 캐시

다음에 설명되는 일부 Function과 기능은 종종 type object를 기반으로 합니다. 이 중 일부를 성능 좋게 구현하기 위해서는 type object별 JIT(just in time) 캐시가 필요합니다. 이는 getJitContainer(type)를 통해 제공할 수 있습니다. 이 Function은 임의의 데이터를 저장할 수 있는 단순한 객체를 반환합니다. 그 객체에 대한 reference를 유지하지 않는 한, Type object 자체가 더 이상 reference되지 않을 때 GC에 의해 자동으로 삭제됩니다.

Reflection Classes

typeOf<>() Function 외에도 Type objects에 대한 OOP 대안을 제공하는 다양한 reflection classes가 있습니다. 이 reflection classes는 class, Interface/object literal, function 및 그들의 직접적인 하위 타입들(Properties, Methods, Parameters)에 대해서만 사용할 수 있습니다. 더 깊은 타입은 다시 Type objects로 읽어야 합니다.

import { ReflectionClass } from '@deepkit/type';

interface User {
    id: number;
    username: string;
}


const reflection = ReflectionClass.from<User>();

reflection.getProperties(); //[ReflectionProperty, ReflectionProperty]
reflection.getProperty('id'); //ReflectionProperty

reflection.getProperty('id').name; //'id'
reflection.getProperty('id').type; //{kind: ReflectionKind.number}
reflection.getProperty('id').isOptional(); //false

타입 정보 받기

타입을 대상으로 동작하는 Function을 제공하기 위해, 사용자에게 타입을 수동으로 전달할 수 있도록 하는 것이 유용할 수 있습니다. 예를 들어 validation Function에서는 첫 번째 type argument로 요청할 타입을, 첫 번째 function argument로 검증할 데이터를 제공하는 것이 유용할 수 있습니다.

validate<string>(1234);

이 Function이 string 타입을 얻기 위해서는, type compiler에 이를 알려야 합니다.

function validate<T>(data: any, type?: ReceiveType<T>): void;

첫 번째 type argument T에 대한 reference를 가진 ReceiveType은 type compiler에게 각 validate 호출 시 해당 타입을 두 번째 위치에 넣어야 함을 신호합니다(왜냐하면 type이 두 번째로 선언되어 있기 때문입니다). 그런 다음 런타임에 정보를 읽어내기 위해 resolveReceiveType Function을 사용합니다.

import { resolveReceiveType, ReceiveType } from '@deepkit/type';

function validate<T>(data: any, type?: ReceiveType<T>): void {
    type = resolveReceiveType(type);
}

불필요하게 새 변수를 만들지 않도록 결과를 동일한 변수에 할당하는 것이 좋습니다. 이제 type에는 type object가 저장되거나, 예를 들어 type argument가 전달되지 않았거나, Deepkit의 type compiler가 올바르게 설치되지 않았거나, 타입 정보 방출이 활성화되지 않은 경우(위의 Installation 섹션 참조)에는 에러가 발생합니다.

English中文 (Chinese)한국어 (Korean)日本語 (Japanese)Deutsch (German)