~this can be seen in the nightly build as of Dec 17 (as well as at playground)~
interface ReadOnlyData {
readonly value: number;
}
interface WritableData {
value: number;
}
function erase(data: WritableData): void {
data.value = 0;
}
const readOnly: ReadOnlyData = { value: 1 };
erase(readOnly); // expected a type error, actual: no problem
~this can't be serious, can it?~
~why is it called readonly?~
~please note there are no type assertions or any other attempts to trick the type system, it just simply doesn't work~
~50+ errors left uncaught~
constructive discussion starts here: https://github.com/Microsoft/TypeScript/issues/13002#issuecomment-271361608
This was described at https://github.com/Microsoft/TypeScript/pull/6532#issuecomment-174356151
@aleksey-bykov readonly
basically means you may not modify it. It guarantees no actual immutability of the binding (heck, even Scala doesn't provide facilities that deep - val name = value
and def name = value
carry the same signature, and toString
is a property). Consider it a case of "we're all consenting adults here", not "you shall not modify this value ever" (the latter is what Object.freeze
is for).
@iashmeadows next time you are buying a car and you read "a sport BMW coupe", you say sweet, just what I wanted, you write a check, get your car and drive it home, next day you are all excited to show it to your friends, you stop at a store to get a 6 pack on the way there, you open the trunk and put your beer in it, when you close it you BMW turns into Honda Odyssey... you are like what? you are all pissed and stuff, you take the beer out and it turns into BMW again, you are like what!!! you call the dealer who sold it to you, they drop on you first, it's normal, then they pick up and say you know it's accoring to issue #6532 where some housewife complained that the trunk of BMW isn't big enough for shopping at Costco and taking kids to the beach, and that's exactly why we made a hard decision to turn iT into Honda Odyssey every time anything is put in its trunk, you are like wtf!!! So you go there and you are like: nothing was said about this shit anywhere! And what you hear back is: well we never said we sell true BMW''s why are you surprised? And people around who saw it are like: hey let's be adults here. F**k what?
Regardless of whether or not I agree on that perspective of readonly
, I have to admit I enjoyed the analogy. 馃槃
@aleksey-bykov In case you missed it, the "we're all consenting adults here" was a reference to a common saying in the Python ecosystem, regarding member privacy, types, etc. :wink:
(Basically, conventions are usually sufficient to imply "you're on your own" for diving into internals, and there's only so much you can do to prevent them from doing it, especially without the use of closures.)
@isiahmeadows
In case you missed it, the "we're all consenting >adults here" was a reference to a common >saying in the Python ecosystem, regarding >member privacy, types, etc. 馃槈
Python gets a lot of things wrong. I never understood the appeal and popularity of that language. Irregardless that doesn't make it a good basis for anything.
It guarantees no actual immutability of the >binding (heck, even Scala doesn't provide >facilities that deep - val name = value and def >name = value carry the same signature, and >toString is a property).
I don't see the relevance, val
and def
both mean the _reference_, to a property method or variable, may not be reassigned. They differ in eagerness vs laziness and some other characteristics, type inference characteristics for example, but they don't have different mutability. With respect to deep immutability, which is not what is asked for here, Scala does _allow_ it, you just have to use the right modifier, val
_or_ def
, all the way down and not _var_. Scala does enforce this at the type system level.
Regardless, Scala has a nominal type system with support for optional explicit structural subtyping. Furthermore it doesn't permit these types of readonly violations. I don't think it is the right analogy.
I'm not saying TypeScript made a bad decision here, but personally I think it would have been great if readonly
had resulted in code emit using defineProperty({ writable: false, ... })
, at least it would have made classes useful. But that's a whole other argument.
Type driven emit would be nice in many scenarios, but even if we never get that, a type error here would be useful. Do readonly
properties only get checked for reassignment within class members right now?
Re: comments above, the issue here is not to do with emit, or the fact that readonly doesn't mean const. It's just the looseness of type checking between the interfaces with mismatched properties. If a property could be explicitly marked mutable
, it would not be satisfied by a readonly
property and the conversion disallowed.
here is a dichotomy: a property is either readonly or writable, there is no other outcome
when we say { x: number }
we mean writable x
when we say { readonly x: number }
we mean x
is only for reading
what else do we need? what is the third state implied by mutable
m
That would be the ideal if the readonly
modifier had existed forever. The problem is backward compatibility. To quote @ahejlsberg in the comment linked to above:
Specifically, we can't interpret the absence of a readonly modifier to mean read-write, we can only say that we don't know. So, if an interface differs from another interface only in the readonly modifiers on its properties, we have to say that the two interfaces are compatible. Anything else would be a massive breaking change.
So now we have a trichotomy:
{ readonly x: number }
- x
is definitely not writable
{ mutable x: number }
- x
is definitely writable
{ x: number }
- x
may or may not be writable - coder has not specified
If mutable
was added to the language then your:
interface WritableData {
mutable value: number;
}
would no longer be compatible with ReadableData
. It would allow us to specify definite mutability wherever we needed that safety.
I think of this as similar to the bivariance compromise in TS today. Any fix to it in the future will allow us to improve the situation where we ask for it, but the default will have to continue to be bivariance, to avoid breaking so much existing user code.
(OTOH as TS 2.0 was a major version bump, maybe a massive breaking change would be acceptable to some, but TS is more cautious than that, which is a good thing IMO).
@aleksey-bykov Indeed a mutable
modifier is effectively meaningless. Everything is mutable unless otherwise indicated.
@danielearwicker specifically for that reason there should not be new syntax because the old syntax will have to mean what the new syntax means implicitly and indefinitely. All this will do is create cognitive load
Specifically, we can't interpret the absence of a readonly modifier to mean read-write, we can only say that we don't know. So, if an interface differs from another interface only in the readonly modifiers on its properties, we have to say that the two interfaces are compatible. Anything else would be a massive breaking change.
It could be behind a flag : usually it's a convenient way to handle these kind of breaking changes.
@aluanhaddad
because the old syntax will have to mean what the new syntax means implicitly and indefinitely.
But that isn't what the old syntax meant. { name: string; }
didn't mean that name
was definitely mutable, and so it cannot be changed to mean that now. The world is full of TS 1.x code that uses such declarations for things that should not (or cannot) be modified because that code was written before TS 2. You can't change the meaning of all that code now.
All widely used languages are products of evolution and contain some scars of that process. Only academic niche languages that no one uses are totally spotless.
Look at it this way: suppose functional programming takes over the world and everyone's properties are all now readonly
. Now it looks like readonly
should have been the default, to remove all the noise of everything being marked readonly
. But that change would break most existing apps utterly.
@abenhamdine - yes though I expect there would be implications for maintaining type definition files so they can be used either way. If we have readonly
and mutable
then all type definitions must explicitly say which their properties are, so they are not changed by the compiler switch.
So even then, we'd still need mutable
.
But that isn't what the old syntax meant. { name: string; } didn't mean that name was definitely mutable, and so it cannot be changed to mean that now. The world is full of TS 1.x code that uses such declarations for things that should not (or cannot) be modified because that code was written before TS 2. You can't change the meaning of all that code now.
It depends on how you look at it. TypeScript already changed the meaning of a lot of code by introducing the readonly
modifier. Recall that TypeScript could not previously model readonly properties like the name
property of a function. That meant that it was legal to write
function f() {}
f.name = 'g';
which has no effect under sloppy mode and throws in strict mode. Either way the code was wrong but there was no way to model that.
This also implies the prevailing assumption was mutability which makes perfect sense given the nature of JavaScript.
@danielearwicker
{ name: string; }
didn't mean that name was definitely mutable
no, this is exactly what it meant as far as expressive power of TS at that time, everything used to be modeled with mutable properties (even things that were not supposed to be mutable by design)
You can't change the meaning of all that code now.
exactly, leave the old code alone, readonly is an opt-in thing, if you want it only then you use it
how can it break anything if it's not in your code?
there is no single reason for delivering this feature halfbaked and use "breaking change" as an excuse
whoever stuck in 3 years behind:
lib.d.ts
before readonlyreadonly
lib.2017.d.ts
with readonly and remove them wherever they feel to tightbut regardless of how you go about delivering a new "opt-in" feature, it's not an excuse to cut it short in favor of easier transition
@aluanhaddad
Recall that TypeScript could not previously model readonly properties like the name property of a function... which has no effect under sloppy mode and throws in strict mode. Either way the code was wrong but there was no way to model that.
But that's a great use of a breaking change - to turn a runtime error into a compile time error. Like you say, the code was wrong, and now that can be statically discovered.
Is anything being discussed here that wasn't already covered in #12 or #6532?
@aleksey-bykov
how can it break anything if it's not in your code?
Because it's in type definitions your code uses.
@RyanCavanaugh - It seems not.
how can it break anything if it's not in your code?
@aleksey-bykov I believe this is because readonly
applies implicitly in some scenarios. Specifically, get accessors without matching setters. Existing TypeScript code now behaves as though certain properties were readonly
, regardless of whether you ever add readonly
to your code.
{ x: number } - x may or may not be writable - coder has not specified
you cracked me up :) so when you see { x: number }
and you need to assign x
it basically means the end of the work day for you because you can't make a decision based on how you defined it
@danielearwicker @masaeedu
let me share a secret:
--noLib
allows you to ignore new type definitions and use the old onesreadonly
problem solved
Cool, issue closed then!
@RyanCavanaugh I suppose not.
An off by default, option in the vein of --enforceReadonlyAssignability
, would be a very nice thing to have. It is of course a lot of time and effort, and I do not take that lightly, but it would be very beneficial. It seems as relevant, but of course less broadly valuable, as --strictNullChecks
from a code correctness point of view.
in support of the previous speaker, the topic was renamed
Tracking this at #13347
To be honest I miss the old title. It was so much more memorable. These days, when I get frustrated with readonly
and subsequently try to search for this issue, I actually have hard time finding it.
Most helpful comment
@iashmeadows next time you are buying a car and you read "a sport BMW coupe", you say sweet, just what I wanted, you write a check, get your car and drive it home, next day you are all excited to show it to your friends, you stop at a store to get a 6 pack on the way there, you open the trunk and put your beer in it, when you close it you BMW turns into Honda Odyssey... you are like what? you are all pissed and stuff, you take the beer out and it turns into BMW again, you are like what!!! you call the dealer who sold it to you, they drop on you first, it's normal, then they pick up and say you know it's accoring to issue #6532 where some housewife complained that the trunk of BMW isn't big enough for shopping at Costco and taking kids to the beach, and that's exactly why we made a hard decision to turn iT into Honda Odyssey every time anything is put in its trunk, you are like wtf!!! So you go there and you are like: nothing was said about this shit anywhere! And what you hear back is: well we never said we sell true BMW''s why are you surprised? And people around who saw it are like: hey let's be adults here. F**k what?