Typescript: New Feature Requesting for `const` Parameter

Created on 15 Sep 2017  路  33Comments  路  Source: microsoft/TypeScript

I have a small new feature to request. Let say we have this piece of code:

function setButton(button: Button | undefined) {
    if (button) {
        button.onclick = () => {
            // tsc complains button might be undefined(well done!), 
            // because it might be set to undefined later on,
            // as you can see below `button = undefined;`. 
            button.setAttribute('disabled', 'true'); //error
        }
    }
    // some other code goes here...
    // and eventually, button is set to undefined.
    button = undefined;
}

Fortunately, there is a way to fix this:

function setButton(button: Button | undefined) {
    const constButton = button;
    if (constButton) {
        constButton.onclick = () => {
            // now tsc is satisfied.
            constButton.setAttribute('disabled', 'true'); 
        }
    }

    // impossible to assign to a const variable.
    // constButton = undefined;
}

But can we twist the code a little bit, which is what I asking for? Something like this:

function setButton(const button: Button | undefined) {
    if (button) {
        button.onclick = () => {
            button.setAttribute('disabled', 'true'); 
        }
    }
}

So you can see there really are some cases a const parameter might come to handy. Thank you.

P.S. Just for demonstrating the point, here's a full demo:

class Button {
    onclick?: () => void;
    performClick() {
        if (this.onclick) this.onclick();
    }
    setAttribute(key: string, value: string) {
        //set attribute to this button
    }
}

function setButton(button: Button | undefined) {
    if (button) {
        button.onclick = () => {
            button.setAttribute('disabled', 'true');
        }
    }
    button = undefined;
}

const button = new Button();
setButton(button);
button.performClick();
Awaiting More Feedback Suggestion

Most helpful comment

I really want this

All 33 comments

This is essentially covered by #17502 and related to #9741 specifically this comment

@kitsonk Oh, thanks. I see, and let's say this is another useful example, which affects the way we code.

tslint has a rule for this.

@andy-ms That may be a little overdo, since there are some other parameters need reassignment.

I think better to use readonly keyword for this proposal.

function foo(readonly constantProperty: any) {
  constantProperty = 1 // Error. Couldn't assign a readonly parameter.
}

@ngolin tslint has options for overriding errors. I can't imagine this is common enough for that to be burdensome?

The no-parameter-reassignment rule of tslint I would say is not as flexible as what this issue proposes because what the rule does is that we have no choice but enforcing const on all parameters which is quite unfavourable. So, highly recommend implementing this.

@yxliang01 You can add // tslint:disable-next-line no-parameter-reassignment in front of any function that needs mutable parameters. But keep in mind that reassigning the parameter to a function does not change the value for the caller, so it's equivalent to creating a mutable local variable initialized to the parameter.

Thanks @andy-ms . However, it's still not ideal... I think it will make the code messier. Also, there's no control over individual parameters...

@brn readonly can be used to declare parameter properties in constructors. Changing that behavior is a breaking change. And changing the effect of readonly depending on the containing function/method/constructor is not a good thing either.

@ajafff why should it be a tslint rule instead of a declaration like what other languages do?

@yxliang01 readonly on a constructor parameter has a substantive meaning for a declaration. Everything discussed here would be erased from declarations and would only be visible to source level consumers. Even at that point, it's just noise, completely meaningless, except during the period of time that you are editing the the body of the specific function declaring the parameter.

Even at that point, it's just noise, completely meaningless, except during the period of time that you are editing the the body of the specific function declaring the parameter.

That _could_ be argued about the whole type system. 馃槈 It is very arguable that re-assignment of parameters is something that an end user would want to control, as it can lead to logic errors in the code, and is the main goal of TypeScript.

On the other hand, I would much rather see #17502 more fully addressed, because it feels like is addressing the problem space versus a point feature.

I meant it is noise in the interface declaration. since mutation of the parameter inside the method, which is a terrible practice, does not affect the caller the information is not useful. so if I'm consuming something from source and I see a bunch of annotations on parameters stating that their immutable but I'm not changing the code of those functions the information is not helpful to me. I suppose it could be argued that its presence indicates potentially higher quality or more thoughtfully written functions.

everytime I look at some Java code, which fortunately isn't very often, they strike me as implementation details exposed in the same location as a functions interface. this doesn't apply to type information in general. It does apply to parameter destructuring, which is also noise for the source level consumer, but I digress...

Anyway, Scala got this one right.

@yxliang01 I'd like to see this as part of the language. I just don't think readonly should be used for it, because it already has a different meaning.

Until this is implemented in TypeScript, you can use the lint rule I just wrote: https://github.com/ajafff/tslint-consistent-codestyle/blob/master/docs/const-parameters.md. In contrast to tslint's no-parameter-reassignment you enable it for individual parameters using JsDoc /**@const*/

Most functions do not assign their parameters. A systematic use of const or readonly for each formal parameter seems too verbose. Who really uses the final keyword in Java for formal parameters?

I think this kind of assignment should be impossible. However, this could break a lot of code. A compiler option seems the best option... or the use of a linter?

I certainly need this one! :+1:

@Conaclos

Who really uses the final keyword in Java for formal parameters?

I put final everywhere, and everyone should do that.
And lately, that's why I switched to Kotlin, final by default for arguments.

@lppedd
I use final as much as possible except on formal parameters. Because I'm using only read-only parameters, it could be very verbose to mark every formal parameter as such. IDE and linters often offer a setting to enforce it. Moreover, in most of source codes, formal parameters are read-only. Thus, I think that programmers consider them as read-only.

Nice to know that Kotlin do that right.

Are these issues reflected in typescript? Or should we use only tslint? I hope these functions are supported by typescript. But I wonder if the issue is closed and is not being discussed anymore

Searching around google and got here.

I feel like this should be part of ts instead of tslint. Is it still under discussing?

I really want this

~I have a need for this~ edit: I stand corrected!


I think my use case is slightly different from the ones described above

I have a function with a type signature similar to this (much simplified)

const fn = <T> (x: T) => ({ key: x as T });

When I call fn with a string, e.g. fn('someString'), I need for the returned type to be "someString" rather than being widened to string, because I'm using the key as the key in a map, and I want typescript to know all the keys on that map so access to that map can be type-safe

I would like a way to enforce that fn is being called with a const otherwise the type data on the map is no longer accurate

Something like

const fn = <T> (const x: T) => ({ key: x as T });
// or
const fn = <T> (readonly x: T) => ({ key: x as T });

would be great

@cheapsteak

type UnitsOnly<T> = string extends T ? never : T;
const fn = <T extends string>(x: UnitsOnly<T>) => ({ key: x });

fn("ok");
fn("not ok" as string);

Playground Link

I think this is a good idea.
Backing people in other comments - the "readonly" keyword is a good idea.
This is sort of like what C++ have.

I want this. Why is this closed?

this is an excellent idea, I really want this now.

@ngolin could you please reopen this?

I've read this, and skimmed #17502, as well as #9741. I don't think my argument is covered anywhere else, and I think it's a reasonably good one.

I use this pattern a lot:

ts function arrFind(obj: string[], term?: string) { if (!term) { return DEFAULT_RESULT; } return obj.find(x => x.includes(term)); }

This will fail because the includes method doesn't accept undefined. Now, we already narrowed away undefined with the early return statement, but it doesn't matter because term isn't technically const, even if our linter yells at us for assigning to it.
That means all narrowing is lost inside the arrow function. What I wind up doing in practice is

ts function arrFind(obj: string[], term?: string) { const searchTerm = term; if (!searchTerm) { return DEFAULT_RESULT; } return obj.find(x => x.includes(searchTerm)); }

This is added verbosity for the sake of making the compiler happy, and I probably did it 200 times in my current project. If I could simply declare the parameters as const, or better yet if const was the default behavior (as it should be!), I could get rid of all those pointless aliases.

I don't see a reason for #18497 to have been closed, so I've reopened it.

Thanks for reopening it :+1:

That said, this can be implemented by a lint rule (e.g. make all params consty by default, have an opt-out through some mechanism), so the bar is a bit higher in terms of whether we would do it or defer to eslint.

@RyanCavanaugh i understand, from my point of view TS is more reliable for this implementation than using an ESLint rule, maybe am I wrong :thinking:

I don't understand how a lint rule would make the OP's example code work without introducing an extra variable, as in the second code block (constButton). If it's not possible to declare formal params as const, no amount of linting can (or should!) make the typechecker apply narrowing in a nested scope.

The thing we've discussed (offline?) is just having a setting for "all parameters are always const", since in practice it isn't much of a burden to always write code that way. Introducing an actual inline const keyword is unlikely to happen due to this being an area clearly in TC39's domain.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dlaberge picture dlaberge  路  3Comments

zhuravlikjb picture zhuravlikjb  路  3Comments

CyrusNajmabadi picture CyrusNajmabadi  路  3Comments

Antony-Jones picture Antony-Jones  路  3Comments

uber5001 picture uber5001  路  3Comments