Open bradzacher opened 7 years ago
Duplicate of #13721 Duplicate of #6532
Adding expression level syntax is anti pattern for TypeScript. These have other approaches which are erasable or provide sufficient downstream meta data for further optimizations.
How is it a duplicate of #13721? That issue is entirely about adding a comment to emitted code so that uglyifyjs can optimise it away? This is about adding features to the typescript language, pre compilation. In fact it emits no different code.
Similarly for #6532, that issue only pertains to reference assignment. You can still mess with the underlying object, which is the entire problem that I am attempting to solve here.
The problem is that there is no way to do compile time checks to ensure that an object has not been modified (which causes issues such as #16389 where the compiler cannot easily be sure that an object has been unmodified).
Adding expression level syntax is anti pattern for TypeScript.
Like assertion operator !
? :P
@bradzacher: interestingly the checker actually has some isSideEffectFree
check.
What's the difference between immutable
and the generic type helper Readonly
, and its many siblings?
Readonly<T>
can only be applied to an interface.
readonly
applies to properties on an interface.
As described in the proposal, the idea would be that immutable
works like so:
when a variable is declared with immutable
, it acts like const
.
when a property is declared with immutable
, it acts like readonly
.
Except in both cases it also:
Readonly<T>
to objects.pure
functions.pure
methods to be called on the variable (i.e. cannot call push
on an immutable
array).The two keywords pure
and immutable
are intended to be used together to enable compile-time validated, side-effect free code.
The concepts of immutability and readonly shouldn't be confused - if you have a reference to an immutable array, you can be sure its contents won't change, but a reference to a readonly array may be an alias for an object which someone else has a non-readonly reference to (thus its contents can observably change).
I understand that the compiler can't enforce immutability so far, but with Readonly
it seems like immutable
is just an alias for
const a: Readonly<T>
One thing that I see with this, is that you can't use Readonly
with primitives, but I think that's in a PR or something, so soon we will have that option too.
Still, I know this is not real immutability, but I think this is somewhat better, as it allows you to work as you wish.
Readonly<T>
only applies to one level.
i.e.
interface One {
prop : Two
}
interface Two {
otherProp : number
}
const x : Readonly<One> = {
prop: {
otherProp: 1,
},
}
// compiler error
x.prop = { otherProp: 2 }
// compiles fine!
x.prop.otherProp = 2
the idea is that immutable
applies recursively to a variable, its properties, and their properties, etc.
In that case, immutable
would could be represented by something like.
type DeepReadonly<T> = {
readonly [K in keyof T]: DeepReadonly<T[K]>
}
const y : DeepReadonly<One> = {
prop: {
otherProp: 1,
},
}
// compiler error
y.prop = { otherProp: 2 }
// compiler error
y.prop.otherProp = 2
Note however that DeepReadonly<T>
doesn't work as it breaks things like functions.. This can probably be solved with some conditional type wizardry from 2.8.x, but I need to read the spec for that to learn more...
If something like DeepReadonly<T>
makes it into the typescript base defs so it's globally available, then that's one use case of immutable
taken care of.
What you don't get is that immutable
would prevent impure
methods from being called on the object.
For example:
interface One {
prop : Two
mutate : () => void
}
interface Two {
otherProp : number
}
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? (
T[K] extends () => void
? T[K]
: DeepReadonly<T[K]>
)
: T[K]
}
const y : DeepReadonly<One> = {
prop: {
otherProp: 1,
},
mutate() {
this.prop.otherProp = 2
},
}
// compiler error
y.prop = { otherProp: 2 }
// compiler error
y.prop.otherProp = 2
// works fine
y.mutate()
console.log(y.prop.otherProp) // === 2
A good example of this in practice is arrays. Array.prototype.push
is an impure method - it mutates the underlying array. With the immutable keyword, this would be blocked:
immutable arr = [1]
// compiler error - calling impure method on immutable variable
arr.push(2)
There is the ReadonlyArray<T>
interface as part of typescript standard, which is just the Array<T>
type, without the impure functions!
However going through each and every standard API, and creating a separate readonly interface definition for people to use if they choose is cumbersome for the typescript maintainers, and for typescript consumers.
Additionally you would have to rely on package developers to do the same thing for each and every one of their objects (if they don't code pure)...
I was going just mention that, with conditional typing, you could create a DeepReadonly<T>
interface.
I'm thinking about something like this
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? (T[K] extends () => void
? T[K]
: (T[K] extends Array<any> ? ReadonlyArray<T[K]> : DeepReadonly<T[K]>))
: Readonly<T[K]>
};
Is this what you would see with the immutable keyword?
However going through each and every standard API, and creating a separate readonly interface definition for people to use if they choose is cumbersome for the typescript maintainers, and for typescript consumers. Additionally you would have to rely on package developers to do the same thing for each and every one of their objects (if they don't code pure)...
You would likely do the same with this keyword, so I don't see any differences.
You could certainly achieve something close to immutable with the conditional typing. It would be an ugly definition to cover all of the cases, but it'd give you the recursive readonly that you want.
However, you do not gain the ability to ensure no side-effects from methods and functions. Which means the case before with an impure method on the object can still happen (a la [].push
).
Also Readonly<T>
types are assignable to T
, which means this is valid code:
interface One {
prop : number
}
const x: Readonly<One> = {
prop: 1,
}
function impure(arg: One) {
arg.prop = 2
}
// compiles fine
impure(x)
You would likely do the same with this keyword, so I don't see any differences.
You would, but it would be easier for authors to do so.
Rather than having to create a separate Readonly version of each of their types (like ReadonlyArray), they can just annotate their existing types with the pure
keyword (see the array example in the original post).
This is easy for contributors to PR and involves less duplication.
It also means that consuming a library in a pure way is the same as consuming it in an impure way, which makes code easier to understand and onboard on.
Also Readonly
types are assignable to T, which means this is valid code
Maybe this is something that is worth looking, because T is don't assignable to Readonly
An option to check not only the types, but the modifiers of an object, would be a great proposal.
I know that static checking about immutability is a good thing to develop, but this is only for you, and your team, in your project. If you are developing a library, you should not count on the immutability of the compiler, because this is actually JS at the end, and anyone who does not use your library with Typescript, but plain old JS will be capable of mutating your data, there is nothing you can do about it, but use a library to avoid the mutation all together, like Immutable or something.
I want to use Typescript to handle mutation, but I know that at the end, that would work just for me, and I am actually ok with that. That's a feature, not a bug.
@bradzacher Reading this again, I notice that you can have a real Immutable type helper:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? (T[K] extends () => void
? T[K]
: (T[K] extends Array<any> ? ReadonlyArray<DeepReadonly<T[K][0]>> : DeepReadonly<T[K]>))
: Readonly<T[K]>
};
It's a little different to the one I first proposed, and I'm actually assuming that all the objects in the Array are the same shape if they are a tuple, but it works as expected. So, that would cover the immutable keyword.
As the pure
keyword, I think the best way to achieve that would be to allow readonly
keyword in function arguments, and combining it with DeepReadonly
as well. This won't give any errors if you are calling the function as is a side-effect function, TSLint have a rule for that (no-unused-expression), but you wouldn't be able to modify any part of the arguments, in theory that's what makes a pure function pure.
What do you think about that? I really think that even if DeepReadonly
can be provided in userland, it should be included in TS [#14909], and readonly
in function arguments also have an issue. (But I can't find it).
@michaeljota
... but you wouldn't be able to modify any part of the arguments, in theory that's what makes a pure function pure.
Well consider this:
function detonator (readonly destructionCodes: DeepReadonly<DestructionCode[]>) {
if (checkCodes(destructionCodes) {
nuke()
}
}
This functions serious indicators that it performs side-effets (eg. returns nothing, may start a nuclear war, etc). I is certain not a pure function, even tho it doesn't modify arguments.
In addition to @bradzacher's proposal, IMHO, pure functions should explicit return on every paths, returning undefined
migth be ok.
High-order pure functions should be able to require pure functions as arguments, but may return unpure ones.
Also, @bradzacher can pure functions instantiate new
objects?
I have write a similiar issue here: #42758
I like this issue but I do not mix const with immutable.
I suggest to improve this proposal with:
readonlyReference
concept (with the name that you want: immutable, immutable_ref, ro_ref, etc...)pure
functions means readonlyReference
of this
const
with readonlyReference
class Figure{
public name:string,
pure showName(){
console.log(this.name);
}
sufixName(sufix:string){
this.name += sufix
}
}
var figures: Figure[];
var figureRef: readonlyReference Figure;
for(figureRef or figures){ // I need figureRef not to be const but check for not modify referenced object.
figureRef.showName(); // ok because showName is pure
figureRef.sufixName('!'); // bad bacause sufixName
}
Just to encourage some cross conversation with a similar proposal for C# I want to draw attention to the now closed proposal to add a way to denote that a function/method is pure in C# but has been closed since 2017. Making progress in Typescript on alternative ways to improve the safety of applications without having to go full blown Haskell would be quite awesome if it's possible.
Additionally as I've noted in the other discussion Rust Lang appears to have adopted a const
modifier for functions which allows for some of the behavior that I think is being asked for here. With the Deno project which seems to have had a high amount of collaboration between Typescript and Rust development Rust and it's precedence may be a good source of inspiration.
https://blog.rust-lang.org/2018/12/06/Rust-1.31-and-rust-2018.html#const-fn
An example of an implementation of a "purity" system is the contexts and capabilities system built into Hack: https://docs.hhvm.com/hack/contexts-and-capabilities/available-contexts-and-capabilities This system is more flexible than just my "disallow impure actions" proposal.
In the hack system you can opt a function into this system by adding []
before the return annotation
- function foo(): void {
+ function foo()[]: void {
// ...
}
This declares the function as having no capabilities - it has to be completely pure. Within the brackets you can include the name of a "context". Contexts declare what "capabilities" a function can have (for example - can it write properties, or can it do IO operations).
A function with capabilities can only call other functions with the same capabilities. This system is leveraged heavily at Facebook to ensure that codegen pipelines are consistent and stable.
FWIW, I'd be very interested in pure functions along with a compiler check to make sure you do not have unused side-effect free expressions. In some cases I have had to deal with errors where I forgot to return the value of a pure function (I forgot the return
statement) and it would be great if the compiler would remind me that this is dead code in practice so I would notice the error right away.
Right now there is no way to annotate that a function is pure and just calling it without using the return value is a bug.
The aim of this proposal is to add some immutability and pure checking into the typescript compiler. The proposal adds two new keywords that would give developers a means to define functions that are pure - meaning that the function has no-side effects, and define variables that are immutable - meaning that they can never be used in an impure context.
Pure
The
pure
keyword is used to define a function with no side-effects (it is allowed in the same places as theasync
keyword).The keyword should be not be emitted into compiled javascript code.
A pure function:
this.nonPure()
,arg.nonPure()
,nonPure(arg)
, andnonPure(this)
are all disallowed.arg.x = 1
is disallowed within the function body.this
this.y = 1
is disallowed within the function body.Immutable
Similarly a variable may be tagged as immutable:
immutable x = []
(maybe keyword should be shortened toimmut
, or thepure
keyword could be reused for consistency?). This keyword is replaced withconst
in emitted code.An immutable variable:
const
(i.e. its reference may not be reassigned).x.nonPure()
is disallowed.nonPure(x)
is disallowed.const y = x;
is disallowed.const y = { z: x }
is disallowed.x.foo = 1
is disallowed.pureFn(x)
is allowed.x.toString()
is allowed.immutable x = ['a', 'b'] fn(x)
With existing typings
With this proposal, the base javascript typings could be updated to support it. I.e. the array interface would become: