A TypeScript custom transformer which reduces bundle size (with a help of other tools in your bundler - see below) by renaming properties aren't exposed to the public:
function showMessage(opts: { message: string }): void {
alert(opts.message);
}
export function alertMessage(message: string): void {
showMessage({ message });
}
goes to:
function showMessage(opts) {
alert(opts._internal_message); // `_internal_message` will be like just `s` after terser/uglify
}
function alertMessage(message) {
showMessage({ _internal_message: message }); // `_internal_message` will be like just `s` after terser/uglify
}
exports.alertMessage = alertMessage;
You might find the approach pretty similar to how Google Closure Compiler with enabled advanced optimizations works, but you don't need to refactor your project a lot to make it works for Google Closure Compiler (setting up tsickle might be hard as well).
All you need to take all advantages from this tool are:
- Install it
- Setup your compiler to use it
- Setup the tool you use to minify/uglify to mangle properties
This is the next generation of ts-transformer-minify-privates (which allows you rename the only private class' members), which uses some approaches from dts-bundle-generator to detect whether some property is accessible via entry points somehow.
As you might know that you have to pay for everything. The tool was tested on several packages and shows really good results (renames was 100% safe for them), but it doesn't mean that it's 100% safe for your project.
If you rely a lot on duck typing (especially around public level/exports) - it might break your code. But in any way to fix almost all issues I think you can just specify the correct type.
I'd suggest everybody who wants to use it in production 1) run all tests you have 2) if it's possible - look at several (or all of them) compiled files in your project and see whether it's good or not.
I cannot guarantee you that the transformer covers all possible cases, but it has tests for the most popular ones, and if you catch a bug - please feel free to create an issue.
Also, it might not work as expected with composite projects, because the project should contain an entry points you set in options, but it might be in other sub-project. So I'd suggest to test it in non-composite project.
For every property the tool tries to determine whether a property is accessible from entry points you specified in the options.
The property is "accessible from entry point" if the type where this property is declared (including all base classes/interfaces) is directly exported or accessible via entry point (this approach and algorithms are taken from https://github.com/timocov/dts-bundle-generator).
If you rely on duck typing a lot in your project - the tool MIGHT NOT and quite possible WILL NOT work properly, especially it can't detect if the property is exported (because it uses the only explicit "inheritance/usage tree").
Let's say we have the following source code in entry point:
// yeah, it's especially without the `export` keyword here
interface Options {
fooBar: number;
}
interface InternalInterface {
fooBar: number;
}
export function getOptions(fooBar: number): Options {
const result: Options = { fooBar };
const internalOptions: InternalInterface = { fooBar };
console.log(internalOptions.fooBar);
return result;
}
After applying this transformer you'll get the next result:
function getOptions(fooBar) {
var result = { fooBar: fooBar };
var internalOptions = { _internal_fooBar: fooBar };
console.log(internalOptions._internal_fooBar);
return result;
}
exports.getOptions = getOptions;
Even if both Options
and InternalInterface
have the same property fooBar
, the only InternalInterface
's fooBar
has been renamed into _internal_fooBar
.
That's done because this interface isn't exported from the entry point (and even isn't used in exports' types), so it isn't used anywhere outside and could be safely renamed (within all it's properties).
Let's see more tricky example with classes and interfaces. Let's say we have the following code:
export interface Interface {
publicMethod(opts: Options, b: number): void;
publicProperty: number;
}
export interface Options {
prop: number;
}
class Class implements Interface {
public publicProperty: number = 123;
public publicMethod(opts: Partial<Options>): void {
console.log(opts.prop, this.publicProperty);
this.anotherPublicMethod();
}
public anotherPublicMethod(): void {}
}
export function interfaceFactory(): Interface {
return new Class();
}
Here we can a class Class
which implements an interface Interface
and a factory, which creates a class and returns it as Interface
.
After processing you'll get the next result:
var Class = /** @class */ (function () {
function Class() {
this.publicProperty = 123;
}
Class.prototype.publicMethod = function (opts) {
console.log(opts.prop, this.publicProperty);
this._internal_anotherPublicMethod();
};
Class.prototype._internal_anotherPublicMethod = function () { };
return Class;
}());
function interfaceFactory() {
return new Class();
}
exports.interfaceFactory = interfaceFactory;
Here we have some interesting results:
-
publicProperty
, declared inInterface
interface and implemented inClass
class hasn't been renamed, because it's accessible from an object, returned frominterfaceFactory
function. -
publicMethod
hasn't been renamed as well the same aspublicProperty
. -
prop
fromOptions
interface also hasn't been renamed as soon it's part of "public API" of the module. -
anotherPublicMethod
(and all calls) has been renamed into_internal_anotherPublicMethod
, because it isn't declared inInterface
and cannot be accessible from an object, returned frominterfaceFactory
publicly (TypeScript didn't even generate types for that!).
More examples you can see in test-cases folder.
- Install the package
npm i -D ts-transformer-properties-rename
- Add transformer with one of possible ways
Default: []
An array of entry source files which will used to detect exported and internal fields.
Basically it should be entry point(s) of the library/project.
Default: '_private_'
The prefix of generated name for private fields. Private fields might be only is the classes.
Default: '_internal_'
The prefix of generated name for "internal" fields. Private fields might be only is the classes.
Default: 'public'
JSDoc/TypeDoc tag which allows you mark class/interface/property/field/etc as "public" so the tool will not rename it and all its children.
If you'll set it to empty string, the detecting will be disabled.
Example. Let's say we have the following code:
const colors: Record<string, string> = {
red: '#ff0000',
green: '#00ff00',
};
export function getColor(colorName: string): string | undefined {
return colors[colorName];
}
In this case, you'll get _internal_red
and _internal_green
properties, but red
and green
is common names for properties and might be passed via your API.
So, you can add /** @public */
(or any custom tag you set in the options) for colors
constant and the tool leave all properties without any changes:
/** @public */
const colors: Record<string, string> = {
red: '#ff0000',
green: '#00ff00',
};
// ...
Default: false
Whether fields that were decorated should be renamed. A field is treated as "decorated" if itself or any its parent (on type level) has a decorator.
Unfortunately, TypeScript itself does not currently provide any easy way to use custom transformers (see microsoft/TypeScript#14419). The followings are the example usage of the custom transformer.
// webpack.config.js
const propertiesRenameTransformer = require('ts-transformer-properties-rename').default;
module.exports = {
// ...
module: {
rules: [
{
test: /\.ts$/,
loader: 'ts-loader', // or 'awesome-typescript-loader'
options: {
getCustomTransformers: program => ({
before: [
propertiesRenameTransformer(program, { entrySourceFiles: ['./src/index.ts'] })
]
})
}
}
]
}
};
// rollup.config.js
import typescript from 'rollup-plugin-typescript2';
import propertiesRenameTransformer from 'ts-transformer-properties-rename';
export default {
// ...
plugins: [
typescript({ transformers: [service => ({
before: [ propertiesRenameTransformer(service.getProgram(), { entrySourceFiles: ['./src/index.ts'] }) ],
after: []
})] })
]
};
See ttypescript's README for how to use this with module bundlers such as webpack or Rollup.
tsconfig.json:
{
"compilerOptions": {
// ...
"plugins": [
{ "transform": "ts-transformer-properties-rename", "entrySourceFiles": ["./src/index.ts"] }
]
},
// ...
}
If you'd like to minify the properties after renaming them, you can use any of existing tool like uglify-es
/uglify-js
/terser
.
For instance, if you use rollup-plugin-terser
for Rollup, you can add mangle
options to it:
terser({
// ...
mangle: {
properties: {
regex: /^_(private|internal)_/, // the same prefixes like for custom transformer
},
},
})
I think other tools has the similar configuration and you can easily find out how to set it up in your environment.