-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add pure and immutable keywords to ensure code has no unintended side-effects #17181
Comments
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? 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). |
Like assertion operator |
@bradzacher: interestingly the checker actually has some |
What's the difference between |
As described in the proposal, the idea would be that
The two keywords |
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 One thing that I see with this, is that you can't use Still, I know this is not real immutability, but I think this is somewhat better, as it allows you to work as you wish. |
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 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 If something like What you don't get is that 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. immutable arr = [1]
// compiler error - calling impure method on immutable variable
arr.push(2) There is the |
I was going just mention that, with conditional typing, you could create a 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?
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 interface One {
prop : number
}
const x: Readonly<One> = {
prop: 1,
}
function impure(arg: One) {
arg.prop = 2
}
// compiles fine
impure(x)
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 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. |
Maybe this is something that is worth looking, because T is don't assignable to Readonly (or at least, its ReadonlyArray sibling isn't to Array). 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 What do you think about that? I really think that even if |
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 Also, @bradzacher can pure functions instantiate |
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:
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 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: In the hack system you can opt a function into this system by adding - function foo(): void {
+ function foo()[]: void {
// ...
} This declares the function as having no capabilities - it has to be completely pure. A function with capabilities can only call other functions with the same capabilities. |
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 |
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 with
const
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.With objects/interfaces
The keyword(s) should also be allowed in
object
(and by extensioninterface
) definitions:With existing typings
With this proposal, the base javascript typings could be updated to support it.
I.e. the array interface would become:
The text was updated successfully, but these errors were encountered: