Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request: support old ES "with" statement #41051

Open
3 of 5 tasks
ghost opened this issue Oct 12, 2020 · 17 comments
Open
3 of 5 tasks

Request: support old ES "with" statement #41051

ghost opened this issue Oct 12, 2020 · 17 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@ghost
Copy link

ghost commented Oct 12, 2020

I did not thoroughly search for this!
I'm sorry, it's pretty hard to search for such a common word, there were thousands of issues that use "with," but I tried looking through the first few hundred.

Suggestion

TypeScript support (a restricted subset of) the old JavaScript keyword: "with."

Use Cases

Simplifies code that accesses a large number of different, yet known keys of an object in quick succession.

Examples

const foo = {
    bar: "baz",
    qux: "foobar"
};

with ( foo ) {
    // introduces two new variables into scope
    // bar :: string
    // qux :: string
    console.log(bar, qux);
}
// bar : not in scope
// qux : not in scope

output:

const foo = {
    bar: "baz",
    qux: "foobar"
};

{
    console.log(foo.bar, foo.qux);
}

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

~ This is a runtime feature, but this is already a feature that was implemented in ECMAScript itself (at some point).
Technically, it has not been removed. It is merely disallowed in strict mode.

I do not recommend supporting the actual ECMAScript "with" statement (I hate that the self/window/globalThis object is still global).
Instead, I suggest that a very restricted subset is supported.

It would have to be very restrictive in order to not bring back the problems of the original "with."
Suggested restrictions to ensure code can still be typed and remain safe:

  • The object may only have known keys (string literals, no symbols, no numbers), it may not be an array.
  • The object must be contained in a variable, specifically one that is const (function calls can mutate non-const variables).
    (The variable restriction may be alleviated if the code is transpiled into a block with a variable assignment.)
  • (should not be done at global scope?)

It is very possible to transpile with to perfectly safe runtime code that can be guaranteed to be as safe as if one had explicitly accessed the object.
It wouldn't be hard if only supporting a subset, yet there are other implications to consider, such as performance, especially with nested with statements.

(And yes, this could also be used to create the most hellish spaghetti code you have ever seen.)

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Oct 12, 2020
@RyanCavanaugh
Copy link
Member

Despite this having never been supported in TS, I do believe this is actually the first issue requesting support for it. Trailblazing 😅

@ghost
Copy link
Author

ghost commented Oct 12, 2020

...I do believe this is actually the first issue requesting support for it...

I actually am still wary of the spaghetti that TS developers will start cooking if it's supported, but these aren't lazy JavaScript developers, so that's somewhat reassuring. I think we just need more feedback from the community.

@weswigham
Copy link
Member

The with statement is an error in "use strict" js (and therefore modules), so the code you are even allowed to use it in is actually pretty small. Older sloppy-mode scripts is pretty much it.

@ghost
Copy link
Author

ghost commented Oct 12, 2020

The with statement is an error in "use strict" js (and therefore modules), so the code you are even allowed to use it in is actually pretty small. Older sloppy-mode scripts is pretty much it.

Refer to my note under the Checklist:

~ This is a runtime feature, but this is already a feature that was implemented in ECMAScript itself (at some point).
Technically, it has not been removed. It is merely disallowed in strict mode.

I stay within modularized code because it actually makes sense.

Note that I am not suggesting that TS support the statement and just remove type annotations like normal, but instead transpile the statement entirely.

// output after running TSC
with ( foo ) {
    console.log(foo.bar, foo.qux);
}

See how it transpiles variable accesses into object lookups, which should have the same result as if it had not used the with statement at all.

Simply letting the with statement pass unaffected is useless, as often times TSC is run in strict mode, meaning with is a runtime error, as you had mentioned.

@alex-kinokon
Copy link

alex-kinokon commented Oct 13, 2020

By design TypeScript cannot alter the runtime behavior of the code so it’s out of the equation to emit something different from the entered JavaScript code except for downleveling.

@ghost
Copy link
Author

ghost commented Oct 13, 2020

By design TypeScript cannot alter the runtime behavior of the code so it’s out of the equation to emit something different from the entered JavaScript code except for downleveling.

But this is a syntax level part of ECMAScript, just like async/await is, we could classify it as down leveling due to it not being supported by our output target (ES5+). (More like the opposite of down leveling, but you get the idea.)

@RyanCavanaugh
Copy link
Member

Downleveling would be type-directed emit, so if your request is that, it's out of scope

@ghost
Copy link
Author

ghost commented Oct 13, 2020

Dang, guess this classifies as "out of scope" :(

@ghost ghost closed this as completed Oct 13, 2020
@ghost
Copy link
Author

ghost commented Jun 5, 2021

Update this can be done without type based code emit.

input:

const foo = {
    bar: "baz",
    qux: "foobar"
};

with ( foo ) {
    console.log(bar, qux);
}

output:

const foo = {
    bar: "baz",
    qux: "foobar"
};

{
    const __with_scope_0 = foo;

    console.log(
        "bar" in __with_scope_0 ? __with_scope_0.bar : bar,
        "qux" in __with_scope_0 ? __with_scope_0.qux : qux
    );
    // (: bar) in the conditional operator accesses the outer scope's "bar"
}

Example of nested withs:
input:

with ( foo ) {
    with ( bar ) {
        console.log(a);
    }
}

output:

{
    const __with_scope_0 = foo;
    {
        const __with_scope_1 = bar;
        console.log(
            'a' in __with_scope_1 ? __with_scope_1.a : ('a' in __with_scope_0 ? __with_scope_0.a : a)
        );
    }
}

It should always be possible to transpile like this, because JS identifiers are always representable as constant strings, and are always static in the scope. Even if the user brings arbitrary identifiers into scope via eval(), it would be found in the last section of the conditional operator.

The number of with scopes is always static and able to be emit at compile-time, as they cannot be introduced dynamically (e.g. via function calls), and they aren't be able to leak variables outside of their scope.

@ghost ghost reopened this Jun 5, 2021
@ghost
Copy link
Author

ghost commented Jun 11, 2021

Furthermore, types can also be resolved as unions of everything that the conditional operations would return!

@johnsoncodehk
Copy link

I am the maintainer of the official Vue IDE plugin. Due to the particularity of the Vue template syntax, we need to use the with statement to make some very important performance improvements. Considering the number of Vue users, the benefits this brings will be huge. If TS can support this, we would be very grateful. 🙏

@DanielRosenwasser
Copy link
Member

@johnsoncodehk could you elaborate a little bit on that?

@johnsoncodehk
Copy link

johnsoncodehk commented Feb 8, 2022

@DanielRosenwasser Thank you for your attention to this issue!

For the below vue SFC code.

<template>
  <h1>{{ msg }}</h1>
  <button @click="count++">count is: {{ count /* type: number */ }}</button>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
  props: {
    msg: {
      type: String,
      required: true,
    },
  },
  setup() {
    const count = ref(0)
    return {
      count, // type: Ref<number>
    };
  },
});
</script>

We currently need at least these steps to generate virtual code to provide IDE support for script blocks and template blocks:

  1. Generate virtual <script> block script and provide to language service host
// ./virtual_script_code.ts
import { defineComponent, ref } from 'vue'

export default defineComponent({
  props: {
    msg: {
      type: String,
      required: true,
    },
  },
  setup() {
    const count = ref(0)
    return {
      count, // type: Ref<number>
    };
  },
});
  1. Use TypeScript type checker API or language service API to extract component instance type properties.
    • msg
    • count
    • $props
    • $data
    • ...

This is the most time-consuming step, because calculating component instance type after updating the virtual_script_code.ts to the language service host needs to execute a very time-consuming syncHostData().

  1. Generate virtual <template> block script and provide to language service host
// ./virtual_template_code.tsx
import Component from './virtual_script_code.ts'

const componentInstance = new Component();

// component instance properties
let msg = componentInstance.msg;
let count = componentInstance.count;
let $props = componentInstance.$props;
let $data = componentInstance.$data;
// ...

{
  <h1 msg={ msg }></h1>
  <button onClick={ () => count++ }>count is: {{ count /* type: number */ }}</button>
}

(Note that the above process is simplified and is actually more complex.)


If TS support with statement, we can skip the step 2, avoid triggering syncHostData() in the process, and generate more efficient template virtual code.

// ./virtual_template_code.tsx
import Component from './virtual_script_code.ts'

const componentInstance = new Component();

with (componentInstance) {
  <h1 msg={ msg }></h1>
  <button onClick={ () => count++ }>count is: {{ count /* type: number */ }}</button>
}

In our case, we only need language service support in IDE, no need emit js support.

Hope the explanation is clear enough.

@eirikb
Copy link

eirikb commented Mar 10, 2022

Would be cool if this could be used with optional chaining.

In Kotlin this is possible:

x?.y?.apply {
    println(z)
}

Something similar in TS would be nice. In this case not quite as nice, but still usable:

with (x?.y) {
   console.log(z);
}

Only problem is that JavaScript with does not support undefined or null as argument.

@N0tExisting
Copy link

@eirikb this would be possible with:

with (x?.y ?? {}) {
   console.log(z);
}

@eirikb
Copy link

eirikb commented Mar 10, 2022

@N0tExisting Not quite, in that case the with would still run, and probably throw an error for "z not being defined".
Instead I would prefer something like Kotlin, where the block won't be executed.

@ghost
Copy link
Author

ghost commented Apr 24, 2022

Would be cool if this could be used with optional chaining.

In Kotlin this is possible:

x?.y?.apply {
    println(z)
}

Something similar in TS would be nice. In this case not quite as nice, but still usable:

with (x?.y) {
   console.log(z);
}

Only problem is that JavaScript with does not support undefined or null as argument.

Sorry, but yours is an unrelated issue, and I expect should fail to pass the non-goal, "This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)." If anything, how about opening an issue specifically for that request?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

8 participants
@eirikb @DanielRosenwasser @weswigham @RyanCavanaugh @alex-kinokon @johnsoncodehk @N0tExisting and others