Typescript: Add `--inlineConsts` flag

Created on 2 Feb 2016  ·  19Comments  ·  Source: microsoft/TypeScript

New flag --inlineConsts

Input:

const x = 'hello';

console.log(x);

Output:

console.log('hello');

See also #6678 + #6804

TBD: It's not clear you would want longer strings to inline into their use sites. Discuss.

In Discussion Suggestion

Most helpful comment

This feature can actually decrease file size sometimes when combined with tools like uglify. Usually with conditionals:

const myConst = "blah";

// ...

// this code can be safely removed after myConst gets replaced with "blah"
if (myConst !== "blah") {
 // ...
}

All 19 comments

Is this only expected to inline strings/numbers? Regex literals? Template strings?

Only "compile-time computable constants", i.e. all the things that are valid const enum initializers, concatenations of string literals, and true / false.

In the above example, the declaration of the const disappears. That could cause problems in a namespace.

Given a namespace:

// vec3.ts

namespace vec3 {
    export const SIZE = 3
    export function create(x, y, z) {...}
    }

And code that uses it, expecting the constant to be there:

// buffer-helpers.ts

interface VectorType {
    SIZE: number
    create: Function
    }

function read(buffer: Float32Array, type: VectorType) {
    // do something that uses type.SIZE
    }

read(someBuffer, vec3)

Output code, following the suggested rules:

// app.js

(function(vec3) {
    vec3.create = function(x, y, z) {...}
    })(vec3)

function read(buffer, type) {
    // do something that uses type.SIZE
    }

read(someBuffer, vec3)    // will fail in some unexpected manner due to missing vec3.SIZE

Hrm, good point. Maybe it's only safe to remove consts that are defined in the global scope?

TBD: It's not clear you would want longer strings to inline into their use sites. Discuss.

Is the goal to optimize by file size, load/parse speed, or execution speed?

constant folding usually targets execution speed. the file size will probably increase if you have that constant used in a few places.

Hrm, good point. Maybe it's only safe to remove consts that are defined in the global scope?

Ideally, it could still inline _uses_ of a module-scope constant, even if it keeps the declaration there. Would that be sensible?

Only "compile-time computable constants"

What would be a suitable way to determine which functions are _compile-time computable_? Would it be enough to say any method on String/Array/Number/Boolean that doesn't have "locale" in the name?

@RyanCavanaugh
@ander-nz

On https://github.com/Microsoft/TypeScript/issues/6805#issuecomment-192079010, and https://github.com/Microsoft/TypeScript/issues/6805#issuecomment-192067493 maybe we should add some new comment form like: /* (const) {ConstantName} ({ConstantValue}) */.

So:

// vec3.ts

namespace vec3 {
    export const SIZE = 3
    export function create(x, y, z) {...}
    }

Would be adjusted to be:

// vec3.ts

/* (const) vec3.SIZE (3) */

namespace vec3 {
    export const SIZE = 3
    export function create(x, y, z) {...}
    }

For completeness I included the sample client code

// buffer-helpers.ts

interface VectorType {
    SIZE: number
    create: Function
    }

function read(buffer: Float32Array, type: VectorType) {
    // do something that uses type.SIZE
    }

read(someBuffer, vec3)

The compiler would see the comment and emit some helper like this:

var __With = function(Target, Name, Value)
{
        Target[Name] = Value;
        return (Target);
};

Which would be used like:

(function(vec3) {
    vec3.create = function(x, y, z) {...}
    })(vec3)

function read(buffer, type) {
    // Other code
   while (SomeVar < type.SIZE)
   {
         /* Code */
    }
    // Other code
 }

read(someBuffer, __With (vec3, "SIZE", 3)) 

Could that work or am I just missing all the corner cases?

@KingDavid12 Your generated code is semantically equivalent to the below:

vec3.create = function(x, y, z) {...}

function read(buffer, type) {
    // Other code
   while (SomeVar < type.SIZE)
   {
         /* Code */
    }
    // Other code
 }

read(someBuffer, (vec3.SIZE = 3, vec3))

There's 0 constant folding going on here, so you don't have the runtime benefit of minimal computation.

@RyanCavanaugh @mhegazy

Should strings be excluded from the constant propagation after a certain limit? There's usually minimal perf difference in practice, and larger strings sometimes are faster when stored in a variable on the heap.

Not arguing numbers, etc., though. Those can still be folded as normally.

This feature can actually decrease file size sometimes when combined with tools like uglify. Usually with conditionals:

const myConst = "blah";

// ...

// this code can be safely removed after myConst gets replaced with "blah"
if (myConst !== "blah") {
 // ...
}

@isiahmeadows Good point. Maybe what can be done is to inline constants internally in the namespace but leave the definition. That would allow later code using the library to see the constant definition and inline it themselves.

@KingDavid12

On Sat, Oct 29, 2016, 11:02 KingDavid12 [email protected] wrote:

@isiahmeadows https://github.com/isiahmeadows Good point. Maybe what can
be done is to inline constants internally in the namespace but leave the
definition. That would allow later code using the library to see the
constant definition and inline it themselves.

That would have to be done to some extent regardless, in order to properly
inline everything. ;-)


You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub
https://github.com/Microsoft/TypeScript/issues/6805#issuecomment-257096348,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AERrBDkIxxiK_VfMrSh5WxdPNfQFjkIPks5q41_0gaJpZM4HRCgy
.

A const string enum would seem to do this pretty well?

const enum X { x = "hello" }

console.log(X.x);

becomes:

"use strict";
console.log("hello" /* x */);

i 'd like to work on this
but I may only make a minimal implement

i'm going to add a inlineConstantTransformer or add transform on tsTransformer

resolve and mark identifier if it can be inline

update identifier node to resolved node

Is there any problem with this? need help plz 🙏

I think we want to re-evaluate the necessity of this

What about not solving this with a separate flag?\
It's obvious that in most cases it's desired to have only some of const definitions removed at compile-time.

I'd suggest putting the declare keyword to a good use here. Now, the declare keyword is used to, basically, tell the compiler that there is a variable lying around in a global scope. Therefore, the declare keyword doesn't allow any implementation.\
However, we could use this limitation of declare to expand its functionality: if an implementation is followed for declare statements - this implementation is then inlined at compile-time.\
Like this:

// this variable exists somewhere, isn't inlined
declare const hello;

// we want to inline this function's implementation
declare const isHello = (v): v is 'hello' => v === 'hello';
// this is inlined later
declare const world = 'world';

if (isHello(hello)) {
  console.assert(hello[0] === 'h')
  console.log(hello, world)
}
if (hello === 'hello') {
  console.assert(hello[0] === 'h')
  console.log(hello, 'world')
}

There's clearly a use-case for this. For example, I write many utility functions just for type-safety, most of which would evaluate to typeof x === something && x === someOtherThing or Array.isArray(x) && typeof x[0] === something or other very simple and atomic operations. These utility functions can greatly increase final build size, and I'd be happy if I could inline them.

I would not want to inline ALL consts, however. And one of the reasons for this is reference-safety.\
Consider the following:

// someObject.ts
const someObject = {
  type: 'object'
};

export default someObject;
// index.ts
import someObject from 'someObject';

let obj = someObject; // default value for `obj`

if (/* some condition */) {
  // assign something to `obj`
}

// if `obj` is still its default value
if (obj === someObject) {
  obj = something;
}
// etc.

After inlining consts, using proposed --inlineConsts flag, this code would work differently than vanilla JS,\
because JS will create two different objects with value of { type: 'object' }.\
Therefore, the condition if (obj === someObject) will always fail, if consts are inlined, because { type: 'object' } === { type: 'object' } will always be false.

EDIT: here's a playground with a small executable demo of what I mean.

Of course, it's possible to maybe not inline consts that are used like this... but won't it be too much cognitive load to always remember which of your consts are inlined and which are not?

The alternative use of declare would make this so much simpler, I think.

This would actually behave a lot like C++ #define statements, which are also very familiar to many people.

@Raiondesu declare in TS is about declaring bindings, specifically bindings with no immediate value, not changing their instantiation.

In my experience, I've never once stumbled across a use case where a primitive constant would've been better not inlined, even in perf-critical code. If you're worried about duplication, gzip can make quick work of that. About the only two places I could imagine it being useful to not inline a constant is in embedded applications and with very large strings used in multiple unrelated places. In the first, you need enough control that you'd do better (ab)using const enums for constants than using const + --inlineConsts. In the second, you can work around it with wrapping it in an object, but it doesn't appear large strings are guaranteed to make it, either.

@isiahmeadows, I understand your point. It's fair. But...

  1. Personal experience is just that. Personal. If you never needed something - doesn't mean nobody needs it.
  2. All of your solutions are just workarounds, which do not cover the problem to full extent and could lead to poor code quality in the long run.
  3. The semantics of declare are only present in the "type-realm" of TS, so to speak. If it were given semantics in the "value-realm" - these semantics would not overlap with the "type-realm" ones. The most obvious example of a similar keyword is typeof which means one thing for values (getting their string typename) and different for types (getting the original type of a value/instance). The same logic can be applied to declare without hurting anybody (including backwards compatibility):

    • declare in "type-realm" would still be about declaring bindings.

    • declare in "value-realm" would be about declaring an alias for a piece of code.

In my comment I never stated that I'm concerned about code duplication or performance (however your case of randomly placed long strings is actually pretty common and would benefit from a keyword solution).

My case was that it would be useful, if consts could inline not only simple primitives, but also complex expressions (like functions or objects). And that it also could be useful if you could atomically control which consts need to be inlined. If in your practice it's actually useful to inline all of them - I don't see any problem with declare either - it could quickly become a second nature for you to just write declare const everywhere. And this would bring clarity too, because there's now a clear distinction between real (const) and inlined (declare const) values.

In the case of a compilation flag, however, it would not only break the vanilla js behavior (I mentioned it in the previous comment), but also make it a complete disaster for readability, since you'd never know by heart (intuitively) which consts in your code are inlined and which aren't. One of TS goals is to not break the semantics of vanilla js. And introducing the inlineConsts flag certainly would disobey this goal. Adding new semantics to a TS-only keyword (declare), however, wouldn't.

If adding some semantics to declare is too mind-blowing (typeof aside) - ok, we could just add another keyword wastefully (as if there weren't too many of them already).

Was this page helpful?
0 / 5 - 0 ratings

Related issues

manekinekko picture manekinekko  ·  3Comments

uber5001 picture uber5001  ·  3Comments

DanielRosenwasser picture DanielRosenwasser  ·  3Comments

Roam-Cooper picture Roam-Cooper  ·  3Comments

MartynasZilinskas picture MartynasZilinskas  ·  3Comments