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

Suggestion: a way to disable type widening in object literals #20195

Closed
pelotom opened this issue Nov 21, 2017 · 39 comments · Fixed by #29510
Closed

Suggestion: a way to disable type widening in object literals #20195

pelotom opened this issue Nov 21, 2017 · 39 comments · Fixed by #29510
Labels
Committed The team has roadmapped this issue Suggestion An idea for TypeScript

Comments

@pelotom
Copy link

pelotom commented Nov 21, 2017

I understand the use of type widening with let and var variables (I use const ubiquitously so this is never a problem). But with object literals I pretty much never want the fields to be widened, and yet that's the default behavior:

const o = { x: 3 }; // inferred type is { x: number }

The only way I know of to work around this is with casts or type annotations, or by passing the object literal immediately to a function expecting the narrower type, thereby guiding type inference (although if the literal is being returned from a function even that technique won't work).

This is an eternal headache and it crops up in so many places. Would it be possible to add a compiler flag that makes it so that object literals don't get widened? I'm happy to be responsible for adding type annotations in the extremely rare cases where I actually want the widened type.

Edit

It looks like compiler flags are generally frowned upon to solve this problem, so let me amend my suggestion to take account of the discussion below. @mhegazy says,

We have talked in the past about a readonly modifier on property declarations.. e.g.

const o = { readonly x: 3 }; 

this allows the compiler to understand the intent of this object literal property, and not widen.

This would certainly be handy, but on its own it's still not ideal because one ends up writing readonly in a lot of places. Take for example CSS-in-JS objects, which involve many properties whose types are unions of string literals. Every single one of those properties needs to then be marked readonly in order to defeat the scourge of type widening. But if one could declare an entire object literal to have only read-only properties like so:

const o = readonly { x: 3, y: 'hello' };
// o: { readonly x: 3; readonly y: 'hello' }

now this starts to look like a workable solution! So, let this be my amended suggestion: readonly modifiers for both object literal properties as well as object literals themselves, which has the side effect of disabling type widening.

To the argument that readonly and type-widening are separate concerns and should be treated independently, to some extent I agree, but I also feel they are related; this is why the let vs. const rules for type widening exist, and I think that rationale makes sense. I also personally don't mind conflating readonly and type narrowing, because I prefer immutable objects everywhere too, so a syntax like

const o = readonly { x: 3, y: 'hello' };

would be attractive because it kills 2 birds with one stone. I currently don't use the readonly keyword much (even though in spirit I want everything to be read-only) because the cost-benefit ratio of annotating every single property in every single object literal is too high.

@mhegazy
Copy link
Contributor

mhegazy commented Nov 21, 2017

We have talked in the past about a readonly modifier on property declarations.. e.g.

const o = { readonly x: 3 }; 

this allows the compiler to understand the intent of this object literal property, and not widen.

@zpdDG4gta8XKpMCd
Copy link

works today:

const o = new class { readonly x = 3 }

@mhegazy
Copy link
Contributor

mhegazy commented Nov 21, 2017

I do not think the flag is right thing to do. such behaviors should not controlled by flags. The flags we have that alter the behavior are meant to be transitional, in other words, we would like everyone to turn them on, but acknowledge the need for a migration path. In this case, i am not convinced that constdeclaration should mean that all the properties, however deep they are are immutable.. that is not the spec'ed behavior of const, nor what the compiler asserts.

@pelotom
Copy link
Author

pelotom commented Nov 21, 2017

@mhegazy that's still less than ideal, because in the limit it requires writing readonly many many times when what I really want is just a blanket statement across my entire code base.

@pelotom
Copy link
Author

pelotom commented Nov 21, 2017

If readonly could used as a modifier of object literals to make all their properties readonly, I think that would be usable:

const o = readonly { x: 3, y: 'hello' };
// o: { readonly x: 3; readonly y: 'hello' }

Or if it were possible to write a function makeReadonly that could do the same

const o = makeReadonly({ x: 3, y: 'hello' });
// o: { readonly x: 3; readonly y: 'hello' }

@zpdDG4gta8XKpMCd
Copy link

@pelotom you should make it clear of what exactly you expect when you say readonly x: 3, because one thing is to have the type 3 another thing is making x property readonly

reading/writing is orthogonal to widening/narrowing

it's by accident that readonly modifier makes the property type narrowed to the type of the value used for initializing

@pelotom
Copy link
Author

pelotom commented Nov 21, 2017

@Aleksey-Bykov I agree, my concern here is with disabling type widening, and readonly was @mhegazy's proposed means to that end. If there's another way to disable type widening that doesn't require me to use a keyword for every property in an object literal, I'm all for it.

@zpdDG4gta8XKpMCd
Copy link

also consider this

const x = 3;
const o = { x };

@mhegazy mhegazy added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Nov 21, 2017
@zpdDG4gta8XKpMCd
Copy link

because

const x = 3, y = 'a', z = null;
const o = { x, y, z };

is arguably more typing than

const o =  {
    readonly x: 3,
    readonly y: 'a',
    readonly z: null
}

@pelotom
Copy link
Author

pelotom commented Nov 21, 2017

Also I should add that I personally don't mind conflating readonly and type narrowing, because I also prefer immutable objects everywhere, so a syntax like

const o = readonly { x: 3, y: 'hello' };

would be attractive because it kills 2 birds with one stone. I currently don't use the readonly keyword much (even though in spirit I want everything to be read-only) because the cost-benefit ratio of annotating every single property in every single object literal is too high.

@zpdDG4gta8XKpMCd
Copy link

be aware there was a discussion on literal literals (no puns): #10195

@pelotom
Copy link
Author

pelotom commented Nov 21, 2017

@Aleksey-Bykov I appreciate all of your proposed work-arounds; believe me when I say that I'm already making heavy use of such things, and find them unsatisfactory. This issue is for proposing an augmentation of the language which would make such workarounds unnecessary.

@zpdDG4gta8XKpMCd
Copy link

i hear you, first class support would be ideal, i am convinced that literal type literals is the way forward

@pelotom pelotom changed the title Suggestion: compiler option to disable type widening in object literals Suggestion: a way to disable type widening in object literals Nov 21, 2017
@pelotom
Copy link
Author

pelotom commented Nov 21, 2017

I edited the main suggestion to be a proposal about readonly modifiers as a potential solution to the problem of type widening for object literals.

@gcnew
Copy link
Contributor

gcnew commented Nov 22, 2017

For posterity: there is already a POC by @tycho01 in #17785.

@pelotom
Copy link
Author

pelotom commented Apr 12, 2018

Bump. This is still a greatly needed feature. Does the TypeScript have this problem on their radar?

@RyanCavanaugh
Copy link
Member

Always here, always watching

@pelotom
Copy link
Author

pelotom commented Apr 13, 2018

@RyanCavanaugh I'm curious why this doesn't work:

function readonly<O extends Readonly<Record<string, any>>>(o: O) {
  return o
}

const result = readonly({ x: 3 })
// Expected type: { readonly x: 3 }
// Actual type: { x: number }

@zpdDG4gta8XKpMCd
Copy link

he might be referring to this: #10195 (comment)

@masaeedu
Copy link
Contributor

@pelotom There's a similar issue with people using tuples for which they want the literal type inferred. By default TypeScript assumes you'll want to mutate it later, so it infers a widened type. I was looking for something like <readonly>[1, 2, 3] as well.

@masaeedu
Copy link
Contributor

function readonly<O extends Readonly<Record<string, any>>>(o: O) {

Unfortunately the game is already over by the time { x: 3 } is ascribed a type. The inference only goes "forward", so the inferred type of { x: 3 } is independent of the surrounding expression. { x: number } satisfies O's constraints, and we get { x: number } back.

@KiaraGrouwstra
Copy link
Contributor

@masaeedu:

Unfortunately the game is already over by the time { x: 3 } is ascribed a type.

so... #17785? 😅

@qm3ster
Copy link

qm3ster commented Aug 20, 2018

Readonly should be the default 👌 😩

const n = { x: 3 }
n.x++ // BOOM
const o = mut {x: 3, y: true}
o.x++ // Ugh, fine
warning: o.y doesn't need to be mutable, consider const o = {mut x: 3, y: true}

👌 😫

@pelotom
Copy link
Author

pelotom commented Aug 20, 2018

@qm3ster

Readonly should be the default 👌 😩

I would agree if the language were being designed from scratch today, but barring a massive backwards-breaking change I think we are stuck now with readonly being the keyword and lack of it implying mutability.

@qm3ster
Copy link

qm3ster commented Aug 20, 2018

@pelotom We have to keep moving forward.
We can't allow TS's great ecosystem and infrastructure to be abandoned because of some hipsters starting fresh that will release a typechecker with slightly different syntax.
As much as I hate compilerOptions, into compilerOptions it goes.
It doesn't look like it would affect too much code, actually, it just needs to be parsed and printed, inside it can pretend to be a bunch of readonlys.
Oh no, poor .d.ts files that don't have a way of specifying their required TS version/settings 😢

@RyanCavanaugh RyanCavanaugh added the Add a Flag Any problem can be solved by flags, except for the problem of having too many flags label Aug 20, 2018
@cshaa
Copy link

cshaa commented Sep 8, 2018

Ah, I found this issue right after creating a new one on #26979. There I propose a different approach to this, without needing much of new syntax. I don't know whether it's better or not, but be sure to check it out! 😉


Your proposal in a nutshell:

const o = {
  readonly a: 7;
}
o.a = 8; // readonly and wrong type

My proposal in a nutshell:

const o = {
  a: 7 as const
}
o.a = 8; // wrong type!

@cshaa
Copy link

cshaa commented Sep 8, 2018

@qm3ster No, readonly doesn't work on object literals. And that's the topic of this issue, judging by the title and the original post. Also why would you change the basic principles of a functioning language, breaking every dependency on the way? And how is this related to the original post?

I proposed an alternative solution to the problem of automatic type widening in object literals. That has nothing to do with default immutability of everything.

@pelotom
Copy link
Author

pelotom commented Sep 9, 2018

@m93a

Your proposal in a nutshell:

const o = {
 readonly a: 7;
}
o.a = 8; // readonly and wrong type

To be clear, this proposal is also (and primarily, I would say), about marking all of an object literal’s properties readonly at once:

const o = readonly {
  a: 7,
  b: true
}
o.a = 8; // readonly and wrong type
o.b = false; // readonly and wrong type

@dmitrysteblyuk
Copy link

What about a compiler option to make all declared objects readonly by default (with a type flag mutable to undo that)? Would be useful for React+redux I think. As everything is readonly there - props, states, stores etc.

@KiaraGrouwstra
Copy link
Contributor

@dmitrysteblyuk #17785 (comment)

@dmitrysteblyuk
Copy link

@tycho01 well, that's not what I meant. Currently objects properties are mutable by default and we have readonly modifier to change that. I suggested to have a compiler option to kinda reverse that behaviour - objects properties (and arrays) readonly by default, mutable if specified so directly in type declaration.

@cshaa
Copy link

cshaa commented Oct 19, 2018

We understand your proposal. The comment still applies. The proposal is neither a temporary patch for a breaking change in behavior (since mutable-by-default is the prefered way to go), nor a “stricter behaviors that we think users should move to”. That means it's a no-go for the maintainers of TypeScript.

@dmitrysteblyuk
Copy link

dmitrysteblyuk commented Oct 19, 2018

@m93a

since mutable-by-default is the prefered way to go

Preferred by whom? The absolutely major part of code in React projects forbids mutating anything. Angular also doesn't do well with mutability (since it checks only object references changes now). So why do you think it's not stricter behaviours that users should move to?

@KiaraGrouwstra
Copy link
Contributor

KiaraGrouwstra commented Oct 20, 2018

heck, any static typing system doesn't do well with mutation.

So why do you think it's not stricter behaviours that users should move to?

for the record, I do, but they're stuck with all types of JS users ("any valid JS is valid TS!") and don't want their language to bifurcate. let's hope Wasm will save us from this.
that said, I think the Flow guys are pretty big on FP, though it wouldn't help Angular.

@pelotom
Copy link
Author

pelotom commented Jan 5, 2019

This continues to be a huge stumbling block for people trying to learn the language:

https://twitter.com/kentcdodds/status/1081333326290415618

(As well as an annoyance for those who understand what’s going on!)

@Meligy
Copy link

Meligy commented Jan 21, 2019

I've been googling for the right issue for a while. Hopefully this is it.

A variation of the main issue is when the value is provided directly.

For example:

function createAction<TType>(type: TType) {
    return {
        type: type
    };
}

const doStuff = createAction("DO_STUFF")

Ignoring the question about the return of the function createAction, the main problem in this example is that createAction sees the coming argument as a string, not as the string literal type "DO_STUFF".

The main characteristic of this example is that the string is defined in-place where it's passed directly to the function.

There are workarounds, like createAction<"DO_STUFF">("DO_STUFF") or createAction("DO_STUFF" as "DO_STUFF"), or even const type = "DO_STUFF"; createAction(type);, but these are ugly to require in a public API.

This is inspired by this tweet which is about Redux Starter Kit.

@stepanhruda
Copy link

Definitely a common problem. As a workaround, I sometimes use as to narrow the type back to the intended constant. It's definitely not ideal:

// inferred {type: 'potatoes'}
const foo = {type: 'potatoes' as 'potatoes'};

// inferred {type: string}
const bar = {type: 'potatoes'};

Works with e.g. as 3 or as true as well.

@qm3ster
Copy link

qm3ster commented Jan 22, 2019

Also a weird case:

const a = 0 // a: 0
const b = {readonly a} // b: {readonly a: number}

😕

@pelotom
Copy link
Author

pelotom commented Jan 23, 2019

Everyone continuing to comment on this issue should check out #29510.

@RyanCavanaugh RyanCavanaugh added Committed The team has roadmapped this issue and removed Add a Flag Any problem can be solved by flags, except for the problem of having too many flags Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Jan 23, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Committed The team has roadmapped this issue Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.