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

Const contexts for literal expressions #29510

Merged
merged 6 commits into from Jan 30, 2019
Merged

Const contexts for literal expressions #29510

merged 6 commits into from Jan 30, 2019

Conversation

@ahejlsberg
Copy link
Member

@ahejlsberg 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
Copy link
Collaborator

@jack-williams 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
Copy link
Contributor

@j-oliveras 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
Copy link

@pelotom pelotom commented Jan 21, 2019

This is AWESOME. Thank you @ahejlsberg!

@ahejlsberg
Copy link
Member Author

@ahejlsberg 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
Copy link
Collaborator

@jack-williams 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
Copy link

@Meligy 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
Copy link

@qm3ster 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
Copy link

@tal 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
Copy link

@pelotom 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
Copy link

@pelotom 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...

@zpdDG4gta8XKpMCd
Copy link

@zpdDG4gta8XKpMCd zpdDG4gta8XKpMCd 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
Copy link

@ducin 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

@zpdDG4gta8XKpMCd
Copy link

@zpdDG4gta8XKpMCd zpdDG4gta8XKpMCd 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
Copy link

@joshburgess 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
Copy link

@pelotom 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
Copy link
Member

@DanielRosenwasser 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.

@zpdDG4gta8XKpMCd
Copy link

@zpdDG4gta8XKpMCd zpdDG4gta8XKpMCd commented Jan 25, 2019

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

@DanielRosenwasser
Copy link
Member

@DanielRosenwasser DanielRosenwasser commented Jan 25, 2019

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

@Bnaya
Copy link

@Bnaya 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
Copy link

@elektronik2k5 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.

@maartenth
Copy link

@maartenth maartenth commented Mar 25, 2019

This is great! would it also be possible in the future to import a json file as const? A json file contains an object literal, but you cannot mark it as const in the json file itself...

@maartenth
Copy link

@maartenth maartenth commented Mar 25, 2019

Now I see that @qm3ster already asked this on Feb 15...

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

It would be a great addition, because these are now the only object literals you cannot assert as const. The as const syntax may conflict with the existing import as syntax but the good news is that the angle bracket <const> syntax does not conflict with jsx in import statements.

Like:

import <const>data from 'data.json'
@qm3ster
Copy link

@qm3ster qm3ster commented Mar 25, 2019

@maartenth I was thinking

import as const * as data from './data.json'
//or
import * as data from './data.json' as const //my preference, and generally the position in which I would like to cast imports, if that feature ever becomes available.

So, with synthetic default exports that would be:

import as const data from './data.json'
//or
import data from './data.json' as const

And with = require():

import as const data = require('./data.json')
//or
import data  = require('./data.json') as const // once again a winning proposition, looks like normal function call return cast
@scvnathan
Copy link

@scvnathan scvnathan commented May 15, 2019

I'm not sure if this is intentional, and I haven't seen any mention of it, but I've noticed that when constraining to a string, TS (3.4.5) can narrow to the string literal.
For example, in @Meligy's example:

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");

Change the signature to function createAction<TType extends string>(type: TType) and TType gets inferred to "DO_STUFF"

@Meligy
Copy link

@Meligy Meligy commented May 16, 2019

This is awesome. I'm surprised I haven't tried this before or thought it wouldn't work.

If you want to support all types, but be restricted about them (infer the exact literal if it's a string or number or enum), you can do this:

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

Test cases:

const doStuff = createAction("DO_STUFF");

const doStuff2 = createAction(1);

const doStuff3 = createAction(null);

const doStuff4 = createAction(undefined);

enum TestEnum {One, Two}
const doStuff5 = createAction(TestEnum.One);

Seeing it in unofficial TypeScript playground

januswel added a commit to januswel/react-native-web-sample that referenced this pull request May 24, 2019
@MuYunyun
Copy link

@MuYunyun MuYunyun commented Jun 13, 2019

Does @babel/plugin-transform-typescript support this feature 'Const contexts for literal expressions'?

@pelotom
Copy link

@pelotom pelotom commented Jun 13, 2019

@MuYunyun yes... I suspect because it was already supporting any x as foo expression by just erasing the as foo part.

@MuYunyun
Copy link

@MuYunyun MuYunyun commented Jun 13, 2019

@pelotom any x includes ’Reserved words‘?

@pelotom
Copy link

@pelotom pelotom commented Jun 13, 2019

@MuYunyun you mean does foo include reserved words? I'm not sure, I'm just speculating. All I know is Babel seems to transpile as const just fine.

@jcalz jcalz mentioned this pull request Jun 16, 2019
5 of 5 tasks complete
@brandonkal
Copy link

@brandonkal brandonkal commented Jun 18, 2019

I am glad to see this in! I was ready to suggest as self or something along those lines, because it is essentially the same as doing as repeating the declaration:

const theme = {
  big: '10em',
  small: '5em'
} as {
  big: '10em',
  small: '5em'
}

Using a JS keyword feels a little odd but I'm glad to type less regardless.

@rjamesnw
Copy link

@rjamesnw rjamesnw commented Jul 14, 2019

Is there a way to allow constant string to represent string literals? For example:

const moduleName = "../../../Project/src/Mod";

async function doSomething() {
    var mod = await import(moduleName); // (works only if a string literal at the moment)
}

In the case above, "mod" is of type "any" because the compiler doesn't recognize the string literal in the constant moduleName (for literal strings, the types are correctly pulled). I'm not sure if this was an oversight, but it makes sense to allow it, since constant strings cannot be reassigned. The only workaround is to wrap await import("../../../Project/src/Mod"); in a function:

async function getMod() { return import("../../../Project/src/Mod"); }
async function doSomething() {
    var mod = await getMod(); // (method required since const strings cannot be used to import module type)
}    

I may also add, it seems very difficult to import namespaces using dynamic imports, which I think is another terrible oversight.

async function doSomething() {
    var mod = await import("../../../Project/src/Mod"); // (forced to use a string literal to get typings)
    var o: mod.SomeClass; // ERROR, cannot find namespace 'mod'
    // var o: InstanceType<typeof mod.SomeClass>; // workaround1
    // var o: import("../../../Project/src/Mod").SomeClass; // workaround2 (who wants to keep typing full paths? A const string would be nice here.)
}

That doesn't even make sense. A namespace, while a type, is still a reference under the hood, and thus should still be importable dynamically somehow; perhaps like:

async function doSomething() {
    import mod = await import("../../../Project/src/Mod"); // (forced to use a string literal to get typings)
    var o: mod.SomeClass;
}

I think all this would aim to better support dynamic imports "on demand" instead of modules forcibly loading every single module when some may not be needed at all, It could also help promote faster initial page loads in many cases. ;)

@Meligy
Copy link

@Meligy Meligy commented Jul 15, 2019

@rjamesnw since this PR is already merged I don't expect that you can get traction for anything new, but this is a good one actually. Can you please open a new issue for it? Thanks.

@rjamesnw
Copy link

@rjamesnw rjamesnw commented Jul 15, 2019

You’re right, sorry, it also occurred to me and I was going to get to doing so asap. ;)

Update: Issue created: #32401

CKGrafico added a commit to CKGrafico/create-react-app that referenced this pull request Aug 9, 2019
When we want to use other TypeScript version and we do:
`npm install typescript@3.5.3 -D` 
But when we use new features like const casting microsoft/TypeScript#29510 it does not compile.
Instead of that if we use:
`yarn add typescript@3.5.2`
Everything works
@tmlayton tmlayton mentioned this pull request Feb 27, 2020
4 of 4 tasks complete
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment