Repository navigation
Support readonly type operator to allow correct definition of Object.freeze #10725
Description
Activity
There is no readonly type operator; i.e.
freeze<T>(o: T): readonly T;. so there is no way to do this at the time being.We have discussed adding a readonly type operator that would recursively mark all properties as readonly.
Reacted by Paul Koerbitz, Braden Snell, Kagami Sascha Rosylight, Abdel, Adrian Sampson, Tom Crockett, Wint Lu, Adrien Crivelli and Victorien Elvingerupdating the title.
- changed the title
[-]Object.freeze declaration should mark fields readonly[/-][+]Support `readonly` type operator to allow correct definition of `Object.freeze`[/+]on Sep 6, 2016 - addedSuggestionAn idea for TypeScriptAn idea for TypeScriptIn DiscussionNot yet reached consensusNot yet reached consensus
on Sep 6, 2016 In our huge Typescript app, we're using a data storage mechanism that keeps complicated class-based types inside data stores. The rest of the app consumes these objects from the stores, but there's always a risk of developers trying to modify the objects that come out of the stores, which ruins delta checking across the app. We could use object.freeze(), but that plays havoc with JIT.
Instead, we maintain duplicate deep readonly versions of all of our model objects. The stores distribute readonly versions, and if anyone actually needs to update them, they first clone the readonly object back into an impl object, modify it, and then shove it back in the store. The store takes it in and stores it as a readonly version.
The general data access pattern works fantastically. There's no javascript overhead from object.freeze(), and our engineers are unable to do terrible things with our data models (outside of casting, of course...) The big issue is just that we have a ton of these deep readonly models that we're manually maintaining, and also having to have comments to maintain which methods are readonly-accessible. We're used to just using "const" in C++ to solve this problem, both on the accessors (from stores) and for the readonly-safe methods in a class, so this is definitely a step backward, and has introduced some engineer errors by not keeping the two versions in sync.
At least we have readonly at all, it was a huge boon to us to get that. :) I'm just hoping for a true deep "const"-style readonly someday.
Reacted by Ivo Stratev, Aluan Haddad, Stoicescu Cristi, Jakub Kisielewski, Braden Snell, stgatilov, Maxim Kulikov, interphx, Alec Mev, Francis Gulotta and 5 moreasfernandes commented
on Feb 5, 2017 More actionsShould not this be closed after #12114 and
Readonly<T>?It depends if there's any hope of a const-style. We haven't bothered using ReadOnly<> because, to be frank, it's worthless to us. We need deep readonly, so we have just created basically deep readonly types as copies of normal types. I don't really understand how shallow readonly helps, unless all of your interfaces are just a list of primitives, without any deep classes on them.
asfernandes commented
on Feb 5, 2017 More actionsDavid de Regt (@deregtd) I agree with you on worthless of shallow Readonly. But Object.freeze is shallow too and is already changed per this issue.
Fair enough. Should I just open a new one asking for true const?
asfernandes commented
on Feb 5, 2017 More actionsIsn't what #11535 (referenced in this issue) is already asking for?
Kinda. He's asking to mark interfaces as full readonly. I want to have a basic set of interfaces, and then be able to mark certain usages of them as const, and have it be deep C++-style const, without having to make a complete second copy of all of them. All the attached bugs seem to basically be solved by the partial types business.
asfernandes commented
on Feb 5, 2017 More actionsC++-style const does not do what you say when using references and pointers (and that's how js/ts works), for example:
class Parent { public: /*const*/ Child* child; }; const Parent* parent = ...; parent->child->iCanMutateThis = true;
I think the idea should not be about have a const making a variable readonly, but a form of deeply apply something like Readonly<>. See my #13214 and the referenced issue #12424. Unfortunately, seems #12424 is more about theory than something practical.
I understand the reference issue, but there's still a pretty easy way to do a better version of it in TS.
class SubBlah {
c: string;
}class Blah {
a: string;
b: SubBlah;const getA(): string {
return this.a;
}const getC(): string {
return this.b.c;
}setC(val: string) {
this.b.c = val;
}
}function DoStuffConst(d: const Blah) {
const a = d.getA(); //Works
const b = d.getC(); //Works
d.setC('a'); //Doesn't compile
}function DoStuff(d: Blah) {
const a = d.getA(); //Works
const b = d.getC(); //Works
d.setC('a'); //Works now
}David de Regt (@deregtd) what about
type DeepReadonly<T> = { readonly [K in keyof T]: DeepReadonly<T[K]> } type T = DeepReadonly<{ a: { b: number } }> const x: T = { a: { b: 1 } } x.a.b = 2 // error Cannot assign to 'b' because it is a constant or a read-only property.
Reacted by Brent Traut, Paul Craddick, Mariusz Pawelski, Renovator, dave kraftsow, Chris Gervang, Michael Salaverry, Il Hwan You, AwesomeObserver, Florin Cosmin and 5 moreReacted by Sean G. WrightReacted by David de Regt, Matthias and Douglas Gubert21 remaining items
KiaraGrouwstra commented
on Dec 24, 2017 ContributorMore actionsAlexandre Galays (@AlexGalays): the tough part is ideally we'd wanna preserve explicitly defined indices throughout operations, but I think we don't have a way to check for them.
What we can do is try to access them, but if we do while they weren't there, the type just errors, which we cannot currently 'catch'.I just want to submit that ideally a solution here could also solve another problem which is one of my biggest gripes with TypeScript: unnecessary type widening. Since
readonlyfields should not be widened, it seems like literals passed toObject.freezeshould not have their types widened:const o = Object.freeze({ x: 3, y: 'hello' }); // o: { x: number; y: string } // but would ideally be // o: { readonly x: 3; readonly y: 'hello' }
In the linked issue I proposed a
readonlyterm operator for this purpose, but it would actually be preferable if it were possible to express a type forObject.freezethat could accomplish this, since then I could give the same type to a function which did nothing but return the original object, eliminating the runtime overhead if all I want is the static check. Thus being able to typeObject.freezesuch that it inferred the type{ readonly x: 3; readonly y: 'hello' }in the above example would be a strictly more powerful feature and would kill two birds with one stone.Reacted by kiara, SlurpTheo, Bo Lingen, Timur Khazamov, James Bromwell and Landon PochKiaraGrouwstra commented
on Feb 11, 2018 ContributorMore actionsI think the
DeepReadOnlyimplementation in #21316 should mean this issue is now resolved.
All that seems left is to have it added tolib.d.tsand have theObject.freezedefinition adjusted accordingly, though technically they seem outside of what was asked here.One note:
DeepReadOnlyimplementation in #21316 removes all function properties from provided object, butObject.freezedoesn't.Reacted by kiaraNot sure why function props are singled out in #21316. Perhaps to showcase the usage of
NonFunctionPropertyNames<T>? Regardless, unless I'm missing something, this ought to do the trick:type DeepReadonly<T> = T extends any[] ? DeepReadonlyArray<T[number]> : T extends object ? DeepReadonlyObject<T> : T; interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {} type DeepReadonlyObject<T> = T & { readonly [P in keyof T]: DeepReadonly<T[P]> };
Note the union in
DeepReadonlyObject<T>, it takes care of both regular functions and hybrid interfaces-functions, like this one:interface A { (): string; prop: number; }
Just in case, I don't know TS that well, this may have unexpected side effects.
Reacted by Zzzen and Michael De AbreuThis can be closed now that the
freezemethod uses theReadonlyandReadonlyArraygeneric utility types, I take it?Lines 195 to 211 in b36c8a0
/** * Prevents the modification of existing property attributes and values, and prevents the addition of new properties. * @param o Object on which to lock the attributes. */ freeze<T>(a: T[]): ReadonlyArray<T>; /** * Prevents the modification of existing property attributes and values, and prevents the addition of new properties. * @param o Object on which to lock the attributes. */ freeze<T extends Function>(f: T): T; /** * Prevents the modification of existing property attributes and values, and prevents the addition of new properties. * @param o Object on which to lock the attributes. */ freeze<T>(o: T): Readonly<T>; Maybe for the original post, yes, but then I will have to break my follow up post into a separate one for deep read only since that is only one level deep. :)
Yep, should probably close the original issue and open a followup asking for a built-in Utility Type for
DeepReadonly.Since the implementation above and in #21316 (comment) is written by ahejlsberg, maybe it's not too far away from a the quality for inclusion?
maybe it's not too far away from the quality for inclusion?
well type recursion is not deemed a supported use of the language so far :/
I propose the issue be closed now. Read-only array types have been around for a year, which means the following works as expected. Daniel Rosenwasser (@DanielRosenwasser) Ryan Cavanaugh (@RyanCavanaugh)
type DeepReadonly<T> = { readonly [K in keyof T]: DeepReadonly<T[K]> }; type Foo = { a: { b: { c: [ { x: 'y' } ] } } }; const foo: Foo = { a: { b: { c: [ { x: 'y' } ] } } }; type ReadonlyFoo = DeepReadonly<Foo>; const readonlyFoo: ReadonlyFoo = foo; // All type errors readonlyFoo.a = readonlyFoo.a; readonlyFoo.a.b = readonlyFoo.a.b; readonlyFoo.a.b.c = readonlyFoo.a.b.c; readonlyFoo.a.b.c[0] = readonlyFoo.a.b.c[0]; readonlyFoo.a.b.c[0].x = readonlyFoo.a.b.c[0].x;
Reacted by Peter Flynn, David Johnston, Matthew Ha, Ryan Cavanaugh, Chetanya Kandhari, Shlomo and mtoneMartinJohns commented
on Sep 26, 2020 ContributorMore actionsI'd keep it open, but adjust the issue instead. Given the mention of
Object.freezeit's clear that he (and I as well) seek to represent immutability in the type-system, which currently is not supported.RyanCavanaugh commented
on Jul 5, 2022 MemberMore actionsI agree this is effectively solved for the use cases in OP and related posts. New issues welcomed for further scenarios.
TypeScript Version: 2.0@RC
Code
Expected behavior:
Frozen object has
readonlypropertiesActual behavior:
Type system allows me to write to
o's properties