Skip to content

typescript: emitting types info for using in runtime (reflection)

License

Notifications You must be signed in to change notification settings

goloveychuk/tsruntime

Repository files navigation

tsruntime

Build Status npm version

Typescript custom transformer for emitting types for using in runtime (reflection).

Motivation

Sometimes you need types metadata in the runtime, e.g. to validate backend response or set propTypes from interface Props{}

Prerequisites

  • typescript: =>5.0.0

Setup

  • install tsruntime and ts-patch
  • change tsconfig.json:
{
    "compilerOptions": {
        "experimentalDecorators": true, // if you'll use decorators
        "plugins": [
            { "transform": "tsruntime/dist/transform/transformer.js", "type": "program" },
        ],
    }
}
  • run with ts-patch compiler:
    • ts-node --compiler=ts-patch/compiler src/index.ts
    • tspc
    • change compiler in awesome-typescript-loader config
    • etc

Warning: You cannot use transpileOnly compiling mode and isolatedModules (if you want reflect types from imported modules).

Usage:

Using reflect function

import {reflect} from 'tsruntime';

interface StatsModel {
    a?: number
    b: string
    c: Array<string>
    d: number | string | null
}

const type = reflect<StatsModel>()
console.log(type)

const type2 = reflect<string>()

On compiled code you'll have

var type = tsruntime_1.reflect({
  kind: 15 /*Object*/,
  name: "StatsModel",
  properties: {
    a: {
      kind: 17 /*Union*/,
      types: [{ kind: 12 /*Undefined*/ }, { kind: 3 /*Number*/ }]
    },
    b: { kind: 2 /*String*/ },
    c: {
      kind: 18 /*Reference*/,
      type: Array,
      arguments: [{ kind: 2 /*String*/ }]
    },
    d: {
      kind: 17 /*Union*/,
      types: [
        { kind: 13 /*Null*/ },
        { kind: 2 /*String*/ },
        { kind: 3 /*Number*/ }
      ]
    }
  }
})();

Using class decorators

import {Reflective, getClassType} from 'tsruntime';

@Reflective
export class StatsModel {
    a?: number
    b!: string
    c!: Array<string>
    d!: number | string | null
}

@Reflective
class Foo extends Array<string> {

}

console.log(getClassType(StatsModel))
console.log(getClassType(Foo))

On runtime you'll have

// ...
StatsModel = __decorate(
    [
      tsruntime_1.Reflective({
        kind: 19 /*Class*/,
        name: "StatsModel",
        properties: {
          a: {
            kind: 17 /*Union*/,
            types: [{ kind: 12 /*Undefined*/ }, { kind: 3 /*Number*/ }]
          },
          b: { kind: 2 /*String*/ },
          c: {
            kind: 18 /*Reference*/,
            type: Array,
            arguments: [{ kind: 2 /*String*/ }]
          },
          d: {
            kind: 17 /*Union*/,
            types: [
              { kind: 13 /*Null*/ },
              { kind: 2 /*String*/ },
              { kind: 3 /*Number*/ }
            ]
          }
        }
      })
    ],
    StatsModel
  );
// ...

Using types info

import {Types, reflect} from 'tsruntime';

const isString = reflect<string>().kind === Types.TypeKind.String

Full runtime info available

Customization

You can customize both reflect and Reflective to do whatever you want

import {createReflective, Types} from 'tsruntime';

const storage = {} as any

function MyReflective (key: string) {
    return createReflective(reflectedType => {
        return (target: any) => {
            storage[key] = reflectedType
        }
    })
}

// typeof MyReflective('realcls') is MarkReflective type.

@MyReflective('realcls')
class Cls {
    prop = 42
}
// in compiled code - @MyReflective('realcls')({kind: ...})

const clsType = storage['realcls']


const validateResp = createReflective(
  reflectedType => <T>(resp: unknown) => { //should have <T>
    if (reflectedType.kind === Types.TypeKind.String) {
        return typeof resp === 'string'
    }
  }
)

const isValid = validateResp<string>('asd') 
// in compiled code - validateResp({kind: ...})('asd')

How it works

createReflective expects reflectedType => T to be passed as arg.

It returns T and marks it as reflectable (type MarkReflective).

Transformer see that symbol have type MarkReflective and calls it with reflectedType.

For function it gets type from first generic argument (thats why you need <T>), for decorator - from class declaration, related to decorator.