Typescript: Suggestion: a way to disable type widening in object literals

Created on 21 Nov 2017  ·  39Comments  ·  Source: microsoft/TypeScript

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.

Committed Suggestion

Most helpful comment

@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

All 39 comments

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

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

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

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

blendsdk picture blendsdk  ·  3Comments

uber5001 picture uber5001  ·  3Comments

wmaurer picture wmaurer  ·  3Comments

dlaberge picture dlaberge  ·  3Comments

Antony-Jones picture Antony-Jones  ·  3Comments