0.5.10 • Published 11 months ago

ts-keys-turn v0.5.10

Weekly downloads
-
License
MIT
Repository
github
Last release
11 months ago

ts-keys-turn

A TypeScript custom transformer which enables to obtain keys of given type.

Build Status Downloads

Requirement

TypeScript >= 2.4.1

Motivation

Object.keys and Object.getOwnPropertyNames methods return string[] type instead of (keyof obj)[] expected by the naming. The reason why this is done is that the types of objects in typescript are covariant and may implicitly contain supersets of other types. Therefore, they can lead to the fact that in runtime - when calling Object.keys - we can get as a result keys that are not covered by typescript types. For example consider this one that lead to runtime error while typescript thinks that everything is fine.

This package presents safe workaround.

How does it work?

The package contains function keyTransform for transformation Object.getOwnPropertyNames<typeof o>(o) expression to array of keys in result file and overriden type for ObjectConstructor.getOwnPropertyNames that returns Array<keyof T> for safe transformation cases, whenever possible, else - string[]. For example:

let ab = { a: 1, b: 1 } as const
let abc = { a: 1, b: 1, c: 1 } as const
ab = abc
let ks = Object.getOwnPropertyNames<typeof ab>(ab)   // ("a" | "b")[]

will be converted (in compile time) to:

var ab = { a: 1, b: 1 };
var abc = { a: 1, b: 1, c: 1 };
ab = abc;
var ks_2 = ["a", "b"];                             // ["a", "b"] is matches with type

instead of:

var ab = { a: 1, b: 1 };
var abc = { a: 1, b: 1, c: 1 };
ab = abc;
let keys = Object.getOwnPropertyNames(ab)           // <- ['a', 'b', 'c'] <- missmatch with typescript

Constraints:

There are several limitations for security and transparency reasons. The getOwnPropertyNames method return (keyof typeof obj)[] instead of string[] and makes appropriate transformation only when the following rules are followed:

  • Generic type should be explicitly specified in the calling signature. It's kind of a way to choose exactly how to handle the construction during development:
    let ae = { a: 1, b: 1 }
    let strs = Object.getOwnPropertyNames(ae)             // string[]
    let keys = Object.getOwnPropertyNames<typeof ae>(ae)  // (keyof AE)[]
  • The type should contains just required fields to avoid the discrepancy of the list of fields with the runtime: with optional fields:
    type A = { a: 1, b?: 1 }
    let ae: A = { a: 1, b: 1 }
    let ks = Object.getOwnPropertyNames<A>(ae)            // string[]
    with required fields:
    type A = { a: 1, b: 1 }
    let ae: A = { a: 1, b: 1 }
    let ks = Object.getOwnPropertyNames<A>(ae)            // (keyof AE)[]
  • The type should not be union for the same reason:
    type U = { a: 1 } | { a: 1, b: 1 }
    let aa: U = { a: 1, b: 1 }
    let k = Object.getOwnPropertyNames<U>(aa)             // string[]

How to use this package

Properly using the package consists of the three following steps (both of them required!):

  • Installation:

    npm i -D Sanshain/ts-keys-turn

    or

    npm i -D ts-keys-turn
  • Addition of the package to the include section of your tscofig.json (if node_modules didn't...):

     "include": [
       // ...
       "node_modules/ts-keys-turn"
    ]
  • Tuning custom transformer which is used to compile the keys function correctly: look up point "How to use the custom transformer":

How to use the custom transformer

Unfortunately, TypeScript itself does not currently provide any way to use custom transformers by tsconfig.json (See https://github.com/Microsoft/TypeScript/issues/14419) and requires using itself API. The followings are the usage examples of the API with the transformer for the most common cases:

webpack (with ts-loader or awesome-typescript-loader)

See examples/webpack for detail.

// webpack.config.js
const keysTransformer = require('ts-transformer-keys/transformer').default;

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.ts$/,
        loader: 'ts-loader', // or 'awesome-typescript-loader'
        options: {
          // make sure not to set `transpileOnly: true` here, otherwise it will not work
          getCustomTransformers: program => ({
              before: [
                  keysTransformer(program)
              ]
          })
        }
      }
    ]
  }
};

Rollup (with rollup-plugin-typescript2)

See examples/rollup for detail.

// rollup.config.js
import resolve from 'rollup-plugin-node-resolve';
import typescript from 'rollup-plugin-typescript2';
import keysTransformer from 'ts-transformer-keys/transformer';

export default {
  // ...
  plugins: [
    resolve(),
    typescript({ transformers: [service => ({
      before: [ keysTransformer(service.getProgram()) ],
      after: []
    })] })
  ]
};

ttypescript

See examples/ttypescript for detail. See ttypescript's README for how to use this with module bundlers such as webpack or Rollup.

// tsconfig.json
{
  "compilerOptions": {
    // ...
    "plugins": [
      { "transform": "ts-transformer-keys/transformer" }
    ]
  },
  // ...
}

ts-jest

See examples/ts-jest for details. In order to use this transformer with ts-jest, you need to add a wrapper around it like this:

// ts-jest-keys-transformer.js
const keysTransformer = require('ts-transformer-keys/transformer').default;
const name = 'my-key-transformer';
const version = 1;
const factory = (cs) => (ctx) => keysTransformer(cs.program)(ctx);
// For ts-jest 26 use:
// const factory = (cs) => (ctx) => keysTransformer(cs.tsCompiler.program)(ctx);

module.exports = { name, version, factory };

And add it in jest.config.js like this:

  globals: {
    'ts-jest': {
      // relative path to the ts-jest-keys-transformer.js file
      astTransformers: { before: ['src/react/ts-jest-keys-transformer.js'] },
    },
  },

Note: ts-jest 26.4.2 does not work with this transformer (fixed in ts-jest 26.4.3). Also, for versions smaller than 26.2, you need to provide the transformer in an array instead, like this: astTransformers: { before: ['src/react/ts-jest-keys-transformer.js'] }

TypeScript API

See test for detail. You can try it with $ npm test.

const ts = require('typescript');
const keysTransformer = require('ts-transformer-keys/transformer').default;

const program = ts.createProgram([/* your files to compile */], {
  strict: true,
  noEmitOnError: true,
  target: ts.ScriptTarget.ES5
});

const transformers = {
  before: [keysTransformer(program)],
  after: []
};
const { emitSkipped, diagnostics } = program.emit(undefined, undefined, undefined, false, transformers);

if (emitSkipped) {
  throw new Error(diagnostics.map(diagnostic => diagnostic.messageText).join('\n'));
}

As a result, the TypeScript code shown here is compiled into the following JavaScript.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var ts_transformer_keys_1 = require("ts-transformer-keys");
var keysOfProps = ["id", "name", "age"];
console.log(keysOfProps); // ['id', 'name', 'age']

What about Object.keys?

This package by default is configurated to use Object.getOwnPropertyNames signature for keys transformation instead of Object.keys. Why?

The difference among the methods is that Object.keys, unlike Object.getOwnPropertyNames, returns only enumerated properties. It is important to note that typescript itself cannot control the enumerability of properties, since javascript in runtime allows you to change it for those of them that do not have the configurable: false clause set (that is, all properties for which it is not explicitly set). Therefore ts cann't detect the p-roperty state. However, using these properties inside source ts code even outside the enumeration supposes that the fields will still be explicitly described in types, rather than not at all.

When Object.keys more preferred?

Despite the ways to make non-enumerable fields in runtime (via object.create or modification descriptor from getPropertyDescriptor), many developers prefer not to use this feature to make the code more obvious.

Therefore, for them there is no difference, except that the keys consists of a less number of letters. For such cases, it may be reasonably to use keys method for transformations and at all.

Using keys for transformataion requires the following steps:

  • install the package npm i -D ts-keys-turn
  • specify path for keys.d.ts instead of node_modules/ts-keys-compiler at include option of your tsconfig.json:
    "include": [
       "node_modules/ts-keys-compiler/sources/keys.d.ts"
    ],   
  • pass keys as methodName option to transform function (look up how to use the custom transformer if missed):
    keysTransform(program, {methodName: 'keys'})

Impact on performance

On the tested hardware, for 40 files with 1600 lines of ts code (i.e. 64 thousand lines of code, respectively), and 5 corresponding constructs for transformation in each file, the difference in compilation speed did not exceed 10% (~1.550 sec vs ~1.700 sec). But even if it has weight, you can use the transformer only for production mode like this:

typescript({
  transformers: production ? [service => {
     const program = service.getProgram()
     return {
        before: program ? [keysTransform(program)] : [],
        after: []
     }
  }] : []
})

License

MIT

0.5.10

11 months ago

0.5.9

11 months ago

0.5.8

11 months ago

0.5.7

11 months ago

0.5.6

11 months ago

0.5.5

11 months ago

0.5.4

11 months ago

0.5.3

11 months ago

0.5.2

11 months ago

0.5.1

11 months ago

0.5.0

11 months ago

0.4.9

11 months ago

0.4.8

11 months ago

0.4.7

11 months ago