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.
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.
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.
works today:
const o = new class { readonly x = 3 }
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 const
declaration 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.
@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.
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' }
@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
@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.
also consider this
const x = 3;
const o = { x };
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
}
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.
be aware there was a discussion on literal literals (no puns): #10195
@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.
i hear you, first class support would be ideal, i am convinced that literal type literals is the way forward
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.
For posterity: there is already a POC by @tycho01 in https://github.com/Microsoft/TypeScript/pull/17785.
Bump. This is still a greatly needed feature. Does the TypeScript have this problem on their radar?
Always here, always watching
@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 }
he might be referring to this: https://github.com/Microsoft/TypeScript/issues/10195#issuecomment-372864435
@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.
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.
@masaeedu:
Unfortunately the game is already over by the time
{ x: 3 }
is ascribed a type.
so... #17785? :sweat_smile:
Readonly should be the default :ok_hand: :weary:
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}
:ok_hand: :tired_face:
@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.
@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 readonly
s.
Oh no, poor .d.ts
files that don't have a way of specifying their required TS version/settings :cry:
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!
@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.
@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
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.
@dmitrysteblyuk https://github.com/Microsoft/TypeScript/pull/17785#issuecomment-383639234
@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.
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.
@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?
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.
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!)
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.
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.
Also a weird case:
const a = 0 // a: 0
const b = {readonly a} // b: {readonly a: number}
:confused:
Everyone continuing to comment on this issue should check out #29510.
Most helpful comment
@m93a
To be clear, this proposal is also (and primarily, I would say), about marking all of an object literal’s properties
readonly
at once: