Typescript: [Feature Request]: Allow uninitialized const variable declarations when provably safe.

Created on 18 Feb 2020  Â·  18Comments  Â·  Source: microsoft/TypeScript

Search Terms

const assignment

Suggestion

Allow declaration of uninitialized const variables if only single assignments are possible and that
single assignment always happens before any read.

Use Cases

When a variable is effectively const but requires some short logic to determine the value that
is too complex to be a rhs expression but too short to warrant a helper function.

Examples

const participantName: string;
{
    const participantNameField = document.getElementById("name") as HTMLInputElement;
    if (participantNameField == null)
        participantName = "";
    else
        participantName = participantNameField.value;
}

show(participantName);

Checklist

My suggestion meets these guidelines:

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

Most helpful comment

I doubt that it not a runtime feature.

This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)

What would you expect as output? Because if you just erase type you'll get const participantName; which is SyntaxError: Missing initializer in const declaration

What you want looks pretty close to https://github.com/tc39/proposal-do-expressions so you'll have to wait until it reaches at least Stage 3 to get this syntax supported in TS.

All 18 comments

@MartinJohns Some users may prefer that, but others like myself find the readability reduced.

Also, what if it was an arbitrary if-else chain?

I doubt that it not a runtime feature.

This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)

What would you expect as output? Because if you just erase type you'll get const participantName; which is SyntaxError: Missing initializer in const declaration

What you want looks pretty close to https://github.com/tc39/proposal-do-expressions so you'll have to wait until it reaches at least Stage 3 to get this syntax supported in TS.

@IllusionMH I would think after the compiler proved that no context error existed it would emit the same code as let participantName: string;.

For conceptual reference, this feature is similar to what Java does for final fields that are initialized by class contructors.

I understand where you are coming from, but const is a javascript language feature, you are asking typescript to deviate away from how javascript works.

@IllusionMH I would think after the compiler proved that no context error existed it would emit the same code as let participantName: string;.

Then can you make a compelling argument for why you can't just use let in this scenario?

you are asking typescript to deviate away from how javascript works.

I am not. I am asking for an expansion of the TypeScript grammar. TS already does this in many places. For example const f = 3 as Number; is a syntax error in JavaScript but not in TypeScript. I propose that const f; also not be a syntax error in TypeScript. Followed by appropriate context sensitive analysis to flag it as a CSA error if some execution path would cause f to be read before a write or if f is written to two or more times.

Then can you make a compelling argument for why you can't just use let in this scenario?

You can in fact use let in this scenario, but you can also use let in every place you would use a const. By this logic we should advocate removing const from the language because we can just use let instead in every scenario. const is useful though because it more precisely describes the programmer's intent. I'd like also to express this intention in code blocks like the example.

From documentation of const "The const declaration creates a read-only reference to a value."

When I say you are suggesting typescript deviates from javascript I mean you would be breaking this contract. Your const declaration doesn't actually make a readonly reference because the value hasn't been acquired yet, it instead declares that the value will be assigned at some point but will not be reassigned afterwards.

Followed by appropriate context sensitive analysis to flag it as a CSA error if some execution path would cause f to be read before a write or if f is written to two or more times.

I feel like you'd have more traction asking for a new keyword letfinal to implement the "only assigned once" idiom instead of asking const to sometimes be transpiled into let.

Also, it's fairly common in javascript to write short anonymous code blocks that produce one value as just a function that is immediately invoked:

const participantName = (()=>{
    const participantNameField = document.getElementById("name") as HTMLInputElement;
    if (participantNameField == null)
        return "";
    else
        return participantNameField.value;
})()

I'm pretty sure I've seen code that define function RETURN_OF<T>(f: ()=>T){ return f();} just so the statement const participantName = RETURN_OF(() => {...}); is slightly more verbose.

So you are proposing a new TypeScript-specific syntax const v; or const v: T; which would always transpile to let v; while TS prohibits assignments more than once to it?
Personally the const v; syntax is too confusing as it resembles pure JavaScript syntax.

Instead, how about readonly let v; or let readonly v;? As it should transpile to let, it'd be better for the new syntax to be some extension of let.

I recommend, if you don’t want to just define a function for this, just use an IIFE:

const participantName: string = (() => {
    const participantNameField = document.getElementById("name") as HTMLInputElement;
    if (participantNameField == null)
        return "";
    else
        return participantNameField.value;
})();

Fairly similar to your example (only added (() => and )() to what you had), already implemented (so no need to wait for a TS update), valid JavaScript (aside from the : string annotation obviously), and no strange behind-the-scenes changing const to let (which, I agree with most everyone else, would be confusing and misleading).

And honestly, IIFE’s are so idiomatic in JavaScript that I’m kind of surprised they’re considering that do block implementation. This is what I think many JavaScript/TypeScript developers _expect to see_ for this kind of thing, which has value. I assume that they’re coming at it from a “learning JavaScript” perspective, since the idiom isn’t found in a lot of other languages, so it’s surprising to non-JavaScript programmers?

Also, tangential, but your example can be done _fairly well_ as an RHS:

const participantName: string = (document.getElementById("name") as HTMLInputElement)?.value || "";

The mid-expression casting is kind of ugly, but otherwise this is pretty straightforward and idiomatic.

I agree this is effectively a runtime feature, particularly since the prime use case would be fully supplanted by do expressions.

This could alternatively be taken as a TC39 proposal to TDZ const bindings until their first assignment.

I'm confused by its characterization as a runtime feature when I believe it can solely be reasoned about during static analysis and emitted using existing JavaScript components. It is in essence sugar for IIFE.

it can solely be reasoned about during static analysis and emitted using existing JavaScript components

I can't think of a runtime feature this wouldn't be true for.

@tadhgmister

I'm confused why you are applying the contract of a const declaration when const declarations are defined in the documentation you cite as:

const name1 = value1 [, name2 = value2 [, ... [, nameN = valueN]]];

but my proposed declaration is of the form

const name;

These do not overlap.

@RyanCavanaugh Then I don't understand your definition of "runtime feature". :) Non-Runtime feature to me means "I can implement these new semantics in terms of existing instructions in my output language." vs a runtime feature of "I need new instructions in my output language to implement these new symnantics."

I think the aspect to think about is "Would it make sense to take this proposal to TC39?"

The answer for this is unambiguously "yes"; you could have a proposal today for "Allow consts to be initialized exactly once later".

If you went to TC39 with a proposal to "Add regex-based index signatures to object types" they would rightly ask you "Why are you proposing TS features at the JS committee?"

Ah I see. So the argument is why only change TS when you can change JS and thus change TS upstream? I might ask, which is a faster path to my statement being legal TS? That's not retorical. I actually don't know.

Find your favorite TC39 rep, ply them with flowers and candy to support the do expressions proposal 🙂

I'm not going to advocate that this should be added to typescript but I do want to advocate why I think it's reasonable to want this as a typescript feature instead of a runtime feature. @RyanCavanaugh I'd like you to hear me out because this might have positive implications for the future of useDefineForClassFields (end of this comment)

First just want to address my previous statement:

I feel like you'd have more traction asking for a new keyword letfinal to implement the "only assigned once" idiom instead of asking const to sometimes be transpiled into let.

I agree with @uhyo, readonly let makes the most sense for this feature over any other syntax.

If we have the variable let x: string typescript will give an error on an access of x if there is at least one branch of execution where x wasn't previously assigned. So in this example return x gives an error because x possibly undefined:

function demo1(maybe: boolean){
    let x: string;
    if(maybe){
        x = "condition was true"
    }
    return x;
}

this is a typescript feature. - this playground happily logs undefined when run despite not compiling.

Lets say we define a new syntax of readonly let x which works the same as let x with the added constraint that an error is given on x = ... if there is at least one branch of execution where x was already assigned. so this code would issue a similar warning because x is possibly already defined at the assignment:

function demo2(maybe: boolean){
    readonly let x: string;
    if(maybe){
        x = "condition was true"
    }
    x = "desire is for this to be marked as error"
}

This is semantically very similar to readonly properties in classes where those fields can only be assigned at their declaration or in the constructor, the part where it deviates is that readonly properties are currently allowed to be defined multiple times in a constructor:

class Test {
    readonly a: string;
    constructor(maybe: boolean) {
        if (maybe) {
            this.a = "first condition";
        }
        // allowed to be redefined, but possibly a mistake?
        this.a = "forgot else!";
    }
}

So this would be a subtle semantic difference between readonly let and readonly property .... unless a constructor of that format is going to be very problematic for the future of useDefineForClassFields and at some point we need to add a restriction that readonly properties will need to be defined exactly once.

So I'm not going to argue that this should be added for the sake of adding it, but I will argue that if in the future we need to implement "assigned exactly once" for readonly properties it would make a lot of sense to also allow readonly let to follow similar logic.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rwyborn picture rwyborn  Â·  210Comments

OliverJAsh picture OliverJAsh  Â·  242Comments

born2net picture born2net  Â·  150Comments

xealot picture xealot  Â·  150Comments

fdecampredon picture fdecampredon  Â·  358Comments