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

Const contexts for literal expressions #29510

Merged
merged 6 commits into from Jan 30, 2019

Conversation

Projects
None yet
@ahejlsberg
Copy link
Member

ahejlsberg commented Jan 21, 2019

With this PR we introduce const assertions which favor immutability in the types inferred for literal expressions (inspired by suggestions in #10195, #20195, and #26979). A const assertion is simply a type assertion that uses the reserved word const as the type name:

let x = 10 as const;  // Type 10
let y = <const> [10, 20];  // Type readonly [10, 20]
let z = { text: "hello" } as const;  // Type { readonly text: "hello" }

A const assertion establishes a const context in which

  • string, numeric, and boolean literals have non-widening literals types (see #11126),
  • array literals have read-only tuple types (see #29435), and
  • object literals have read-only properties.

Const contexts do not otherwise affect types of expressions, and expressions in const contexts never have a contextual type.

An expression x is said to occur in a const context if x is

  • the operand of a const assertion, or
  • a parenthesized expression that occurs in a const context, or
  • an element of an array literal that occurs in a const context, or
  • the expression of a property assignment in an object literal that occurs in a const context, or
  • a spread expression that occurs in a const context.

Note in particular that const contexts extend into nested array and object literals. For example, the declaration

let obj = { x: 10, y: [20, 30], z: { a: { b: 42 } } } as const;

corresponds to

let obj: {
  readonly x: 10;
  readonly y: readonly [20, 30];
  readonly z: {
    readonly a: {
      readonly b: 42;
    };
  };
};

A const assertion requires the operand to be a string, number, bigint, boolean, array, or object literal, optionally enclosed in one or more levels of parentheses. It is an error to apply a const assertion to expressions of other forms.

Fixes #10195.
Fixes #20195.
Fixes #26979.

@jack-williams

This comment has been minimized.

Copy link
Contributor

jack-williams commented Jan 21, 2019

Did you consider using const as a hook for enabling deep excess property checking for arbitrary union and intersection types?

@j-oliveras

This comment has been minimized.

Copy link
Contributor

j-oliveras commented Jan 21, 2019

A question:
Will be possible to write:

let obj = {
    x: 10,
    z: { a: { b: 42 } } as const
};

to initialize a type like:

let obj: {
  x: number;
  z: {
    readonly a: {
      readonly b: 42;
    };
  };
};

If yes, probably you want to add a test like that.

@pelotom

This comment has been minimized.

Copy link

pelotom commented Jan 21, 2019

This is AWESOME. Thank you @ahejlsberg!

@ahejlsberg

This comment has been minimized.

Copy link
Member Author

ahejlsberg commented Jan 22, 2019

Did you consider using const as a hook for enabling deep excess property checking for arbitrary union and intersection types?

I did not. Can you provide an example of what you mean exactly?

Will be possible to write (object literal with const assertions in some property values)

Yes.

If yes, probably you want to add a test like that.

Sure.

@jack-williams

This comment has been minimized.

Copy link
Contributor

jack-williams commented Jan 22, 2019

@ahejlsberg

Currently neither of these give excess property errors:

const u: { x: number } | { y: string } = { x: 42, y: "helloworld" };
const i: { a: { x: number } & { y: string } } & { b: boolean } = { a: { x: 42, y: "y", z: "excess" }, b: true };

Though these EPC bugs are on the backlog to fix for all literals, there are 2 obstacles:

  1. Adding it would be a breaking change (but a good one IMO).
  2. Adding it might introduce a perf. regression. I believe EPC is done today in a cheap way that can quickly prune large union types. I think fixing the general case requires a full relation check; there is no free lunch to be had.

I'm suggesting that maybe these give errors by using const contexts as a marker to enable complete and correct EPC:

const u: { x: number } | { y: string } = { x: 42, y: "helloworld" } as const; // error 'y' is excess (or some better error)
const i: { a: { x: number } & { y: string } } & { b: boolean } = { a: { x: 42, y: "y", z: "excess" }, b: true } as const; // error 'z' is excess

Basically using const contexts as a way to trial full-EPC in a way that does not introduce any breaking changes. If the trial is successful then full-EPC can be rolled out to all literals with const being ahead of the curve; if the trail does not work out then full-EPC can be removed in const contexts in a non-breaking way.

sheetalkamat added a commit to Microsoft/TypeScript-TmLanguage that referenced this pull request Jan 22, 2019

@Meligy

This comment has been minimized.

Copy link

Meligy commented Jan 23, 2019

Can it be also assumed in certain situations?

Like:

const a: 12; // type is 12
let x = {
  // Can this be `const`? 
  // Either always, or only when it's `readonly x1: a`. It's fine either way.
  x1: a 
};

The situation here being assignment to a constant, and fine to require readonly for this.

Also, one important situation (as mentioned with more details):

function createAction<TType>(type: TType) {
    // Not concerned with return types here, but inferred argument type
    return {
        type: type
    };
}

// Today this gets inferred as string, but can string literal be inferred as itself?
const doStuff = createAction("DO_STUFF");

The situation here being sending a literal (string or number) to a function with argument type being generic and inferred from usage. It'll be nice to avoid const in createAction("DO_STUFF" const).

@qm3ster

This comment has been minimized.

Copy link

qm3ster commented Jan 23, 2019

Next step: allowing importing JSON modules as const

@RyanCavanaugh RyanCavanaugh added this to This Week in Design Meeting Docket Jan 23, 2019

@tal

This comment has been minimized.

Copy link

tal commented Jan 24, 2019

Is the const keyword from somewhere else? To me it's confusing and easy to conflate with the const variable declaration keyword. immutable would be clearer for me.

@pelotom

This comment has been minimized.

Copy link

pelotom commented Jan 24, 2019

Is the const keyword from somewhere else? To me it's confusing and easy to conflate with the const variable declaration keyword. immutable would be clearer for me.

That would require adding a new keyword for dubious benefit. Personally I think readonly would have been preferable to const (since it has the effect of adding readonly modifiers deeply through a tree of types). But const has the advantage of being short 🤷‍♂️

@pelotom

This comment has been minimized.

Copy link

pelotom commented Jan 24, 2019

Maybe literal would do it justice?

let x = 10 as literal; 
const y = 10 as literal; 

But it was already a literal, before you added as literal...

@aleksey-bykov

This comment has been minimized.

Copy link

aleksey-bykov commented Jan 24, 2019

be it a wanker i would not say a thing, but sure let's nitpick and bike shed this thing to the bone, because we can and it's just useless otherwise

@ducin

This comment has been minimized.

Copy link

ducin commented Jan 24, 2019

Question: if the as const clause recursively adds readonly to object literals, why not calling it as readonly? Not a big issue, but introduces some ambiguity with the native ES6 const which has slightly different meaning than TS' readonly

@aleksey-bykov

This comment has been minimized.

Copy link

aleksey-bykov commented Jan 24, 2019

to those who believes in readonly modifiers, will it make your array properties readonly "deeply through a tree of types"?

@joshburgess

This comment has been minimized.

Copy link

joshburgess commented Jan 25, 2019

@ahejlsberg Would there be any way to dynamically concatenate two literal types to produce a new literal type using this const context?

For example, in a function like...

const concatLiterals = <A extends string, B extends string>(a: A, b: B) =>
  `${a}{b}` as const // or (a + b) as const

I'm assuming the dynamic nature of the above here would prevent this from working, and it would still widen the type to string, but this capability is something I've wanted for a while.

It would be great to have some sort of type-level concat helper for literals like this.... so that you could do something like:

(a + b) as (const: A + B)

or

`${a}{b}` as (const: ConcatLit<A, B>)

or something along those lines.

@pelotom

This comment has been minimized.

Copy link

pelotom commented Jan 25, 2019

Apparently as readonly instead of as const was considered and decided against, see the meeting notes. I understand the reasoning, but I still think as readonly is more intuitive 😛

@DanielRosenwasser

This comment has been minimized.

Copy link
Member

DanielRosenwasser commented Jan 25, 2019

I agree with as const being kind of confusing. Maybe the only reason I don't feel as strongly against it is the explanation itself is relatively consistent with the current distinction of const and readonly.

I suspect that we'll end up with a quick fix like Did you mean 'as const'? when someone writes as readonly.

@aleksey-bykov

This comment has been minimized.

Copy link

aleksey-bykov commented Jan 25, 2019

oh let's not forget what readonly really is #13002

@DanielRosenwasser

This comment has been minimized.

Copy link
Member

DanielRosenwasser commented Jan 25, 2019

Somehow I knew exactly what you linked to before I clicked on it.

@Bnaya

This comment has been minimized.

Copy link

Bnaya commented Jan 25, 2019

This is probably out of the scope, but:
it will be nice to have a way to also "kill" methods that mutating objects in place.
Such as: Array.sort, Array.splice, Date.setDate

@elektronik2k5

This comment has been minimized.

Copy link

elektronik2k5 commented Jan 25, 2019

+1 for as readonly because that's what it actually does.
TypeScript already has a high syntax overhead and adding more cognitive load by requiring the user to keep in mind how readonly thing translates and relates to thing as const should be avoided.

@karol-majewski

This comment has been minimized.

Copy link

karol-majewski commented Jan 26, 2019

Great stuff. The community seems to be very excited about it.

These two concerns were expressed in the comments:

  • possibly confusing syntax (as immutable is the most popular suggestion so far)
  • how does one update such a structure?
@pelotom

This comment has been minimized.

Copy link

pelotom commented Jan 26, 2019

  • how does one update such a structure?

One doesn’t. That’s what immutable means.

@ilyaztsv

This comment has been minimized.

Copy link

ilyaztsv commented Jan 27, 2019

agree with

possibly confusing syntax (as immutable is the most popular suggestion so far)

@qm3ster

This comment has been minimized.

Copy link

qm3ster commented Jan 27, 2019

agree with

possibly confusing syntax (as immutable is the most popular suggestion so far)

🤔 maybe as frozen 🤣
I get why const is not breaking at all since it's a reserved word, but do we really need to care that much? About a lowercase type?

To me as const sounds like "as if it was assigned to const on creation".
Which in the current case, sadly is unique types for primitive literals but widened types for objects.

Does it really make sense that

const immutable = {/* expression */}
// is a different type than
{/* expression */} as const
@vmasek

This comment has been minimized.

Copy link

vmasek commented Jan 28, 2019

Will it be possible to add a compiler option that would automatically enable this for all const and also maybe readonly?

Our team already tries to not use mutable operations and have them disabled by tslint-immutable. With a compiler option, it would be great to have immutability mandatory for constants.

Merge branch 'master' into constContexts
# Conflicts:
#	src/compiler/checker.ts
@tycho01

This comment has been minimized.

Copy link
Contributor

tycho01 commented Jan 30, 2019

@vmasek:

Will it be possible to add a compiler option that would automatically enable this for all const and also maybe readonly?

Our team already tries to not use mutable operations and have them disabled by tslint-immutable. With a compiler option, it would be great to have immutability mandatory for constants.

I definitely hope they feel more bullish on this than last year!

@MastroLindus

This comment has been minimized.

Copy link

MastroLindus commented Jan 30, 2019

I am really excited for this change, I have been waiting for something like it for a long time, thanks for delivering it.

I am adding a +1 on the request to have a flag to enable this as a default behavior for const assignments.
The rationale is that people that are going to use this feature are probably willing to use it as much as possible, basically everywhere, and having to manually add the "as const" every time would be both error-prone and cumbersome (but still much better than any other workaround we had so far to achieve something similar)

@RyanCavanaugh

This comment has been minimized.

Copy link
Member

RyanCavanaugh commented Jan 30, 2019

PR targets the wrong branch - I believe you'll have to open a new one. Or you can merge this and then separately merge the feature branch into master

@ahejlsberg ahejlsberg changed the base branch from readonlyArrayTuple to master Jan 30, 2019

@ahejlsberg ahejlsberg merged commit 96706a7 into master Jan 30, 2019

1 check passed

license/cla All CLA requirements met.
Details

@ahejlsberg ahejlsberg deleted the constContexts branch Jan 30, 2019

@aleksey-bykov

This comment has been minimized.

Copy link

aleksey-bykov commented Jan 31, 2019

@ahejlsberg got my code broken after tonight's update, not sure if it's directly related: #29662

@qm3ster

This comment has been minimized.

Copy link

qm3ster commented Feb 15, 2019

Since this was merged, can we maybe get something related for importing JSON modules as const?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment