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

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

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

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

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 commented Jan 21, 2019

This is AWESOME. Thank you @ahejlsberg!

@ahejlsberg
Copy link
Member Author

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

@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 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 commented Jan 23, 2019

Next step: allowing importing JSON modules as const

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

@zpdDG4gta8XKpMCd
Copy link

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

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

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

@zpdDG4gta8XKpMCd
Copy link

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

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Jan 25, 2019

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

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

+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.

@rjamesnw
Copy link

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 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 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 tasks
matthewrobertson added a commit to GoogleCloudPlatform/functions-framework-nodejs that referenced this pull request Sep 22, 2021
This commit refactors the SignatureType enum into an array of strings
declared [as const](microsoft/TypeScript#29510).
This allows the SignatureType type to be expressed as a union type,
which works a bit better when parsing a users provided string.

This is some simple refactoring in preparation for declarative function
signatures.
matthewrobertson added a commit to GoogleCloudPlatform/functions-framework-nodejs that referenced this pull request Sep 23, 2021
This commit refactors the SignatureType enum into an array of strings
declared [as const](microsoft/TypeScript#29510).
This allows the SignatureType type to be expressed as a union type,
which works a bit better when parsing a users provided string.

This is some simple refactoring in preparation for declarative function
signatures.

BREAKING CHANGE: exported SignatureType type is converted from an enum
to a union type
matthewrobertson added a commit to GoogleCloudPlatform/functions-framework-nodejs that referenced this pull request Sep 23, 2021
This commit refactors the SignatureType enum into an array of strings
declared [as const](microsoft/TypeScript#29510).
This allows the SignatureType type to be expressed as a union type,
which works a bit better when parsing a users provided string.

This is some simple refactoring in preparation for declarative function
signatures.

BREAKING CHANGE: exported SignatureType type is converted from an enum
to a union type
@AliN11
Copy link

AliN11 commented Apr 24, 2022

Why would someone use let for declaring a variable which is typed with as const?

let numbers = [1, 2, 3] as const;

@BeautyInNorth
Copy link

Excuse me,why is the error reported?

const arr = [
  { age: 12, fruits: 'apple' },
  { age: 16, fruits: 'cherry' },
  { fruits: 'banana' },
] as const

type Fruits = typeof arr[number]['fruits']

arr.map(({ age, fruits }) => {
  //Property 'age' does not exist on type 
  //'{ readonly age: 12; readonly fruits: "apple"; } | { readonly age: 16; readonly fruits: "cherry"; } | { readonly fruits: "banana"; }'.
  console.log(age)
})

@pelotom
Copy link

pelotom commented Jun 23, 2022

Excuse me,why is the error reported?

Because not every element of the array has an age property.

@AliN11
Copy link

AliN11 commented Jun 26, 2022

I found out that as const does not work after a variable has been declared:

let x = 12;
x = 2 as const;

x = 5;

console.log(x); // 5

Also you'll get no error if the reassigned value is like the previous value:

let x = [1, 2] as const;

x = [1, 2]; // works

@brandonpittman
Copy link

Excuse me,why is the error reported?

Because not every element of the array has an age property.

Is there a way to type an array of objects declared using as const if some of the objects lack properties that other objects have?

@cshaa
Copy link

cshaa commented Aug 8, 2024

image

While as const turned out to be a really nice feature, it does have its drawbacks.

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