Repository navigation
Add pure and immutable keywords to ensure code has no unintended side-effects #17181
Description
Activity
- changed the title
[-]Add "pure" keyword to ensure a function has no unintended side-effects[/-][+]Add pure and immutable keywords to ensure code has no unintended side-effects[/+]on Jul 14, 2017 - Reacted by Brad Zacher, Jonah Snider, Vadim, Dominik Markiewicz, Danil Menkin, kz-gonzalo-curbelo and emdiet
- marked this as a duplicate of Pure annotation in downlevel emits #13721
on Jul 14, 2017 - marked this as a duplicate of Readonly properties and index signatures #6532
on Jul 14, 2017 bradzacher commented
on Jul 14, 2017 ContributorAuthorMore actionsHow 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).
Reacted by クエン酸, Jonah Snider, Dominik Markiewicz and Danil MenkinReacted by クエン酸Reacted by クエン酸- marked this as a duplicate of Type checking element typed arrays #16389
on Jul 14, 2017 KiaraGrouwstra commented
on Aug 10, 2017 ContributorMore actionsAdding expression level syntax is anti pattern for TypeScript.
Like assertion operator
!? :PKiaraGrouwstra commented
on Aug 13, 2017 ContributorMore actionsBrad Zacher (@bradzacher): interestingly the checker actually has some
isSideEffectFreecheck.- addedSuggestionAn idea for TypeScriptAn idea for TypeScript
on Aug 23, 2017 What's the difference between
immutableand the generic type helperReadonly, and its many siblings?- unmarked this as a duplicate of
on Apr 18, 2018 bradzacher commented
on Apr 18, 2018 ContributorAuthorMore actionsReadonly<T>can only be applied to an interface.
readonlyapplies to properties on an interface.As described in the proposal, the idea would be that
immutableworks like so:
when a variable is declared withimmutable, it acts likeconst.
when a property is declared withimmutable, it acts likereadonly.
Except in both cases it also:- essentially automatically, recursively applies
Readonly<T>to objects. - only allows the variable to be passed to
purefunctions. - only allows
puremethods to be called on the variable (i.e. cannot callpushon animmutablearray).
The two keywords
pureandimmutableare intended to be used together to enable compile-time validated, side-effect free code.Reacted by Ionut Cenac- essentially automatically, recursively applies
RyanCavanaugh commented
on Apr 18, 2018 MemberMore actionsThe 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).
Reacted by クエン酸, Vamsi Ampolu, Evan Winslow, ryan-0324 and Ionut CenacI understand that the compiler can't enforce immutability so far, but with
Readonlyit seems likeimmutableis just an alias for
const a: Readonly<T>One thing that I see with this, is that you can't use
Readonlywith 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.
bradzacher commented
on Apr 19, 2018 ContributorAuthorMore actionsReadonly<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
immutableapplies recursively to a variable, its properties, and their properties, etc.
In that case,immutablewould 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 ofimmutabletaken care of.What you don't get is that
immutablewould prevent impuremethods 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.pushis 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 theArray<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)...Reacted by Ionut CenacI was going just mention that, with conditional typing, you could create a
DeepReadonly<T>interface.
I'm thinking about something like thistype 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.
bradzacher commented
on Apr 19, 2018 ContributorAuthorMore actionsYou 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).
AlsoReadonly<T>types are assignable toT, 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
purekeyword (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 (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.
- addedIn DiscussionNot yet reached consensusNot yet reached consensus
on Aug 15, 2018 Brad Zacher (@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
purekeyword, I think the best way to achieve that would be to allowreadonlykeyword in function arguments, and combining it withDeepReadonlyas 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
DeepReadonlycan be provided in userland, it should be included in TS [#14909], andreadonlyin function arguments also have an issue. (But I can't find it).Reacted by Ondratra and Nick EReacted by Preston GarnoMichael De Abreu (@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 Brad Zacher (@bradzacher)'s proposal, IMHO, pure functions should explicit return on every paths, returning
undefinedmigth be ok.
High-order pure functions should be able to require pure functions as arguments, but may return unpure ones.Also, Brad Zacher (@bradzacher) can pure functions instantiate
newobjects?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:
- adding
readonlyReferenceconcept (with the name that you want: immutable, immutable_ref, ro_ref, etc...) - saying that
purefunctions meansreadonlyReferenceofthis - do not mix
constwithreadonlyReference
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 }
Reacted by ExE BossReacted by Martin Johns and Muniz- adding
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
constmodifier 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
bradzacher commented
on Sep 13, 2021 ContributorAuthorMore actionsAn 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.Reacted by Noel Llevares, Rudolph Gottesheim, Emilio, ExE Boss, Muniz, Chris Vaszauskas, Fabio Spampinato, Travis Wagner and Ionut CenacFWIW, 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
returnstatement) 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.I really wish Typescript had this feature, and I think the proposal here seems nice #17181 (comment)
It would also help guide LLMs.
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
purekeyword is used to define a function with no side-effects (it is allowed in the same places as theasynckeyword).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 = 1is disallowed within the function body.thisthis.y = 1is disallowed within the function body.Immutable
Similarly a variable may be tagged as immutable:
immutable x = [](maybe keyword should be shortened toimmut, or thepurekeyword could be reused for consistency?).This keyword is replaced with
constin 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 = 1is 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: