Flow: Support for immutability checks

Created on 18 Nov 2014  路  23Comments  路  Source: facebook/flow

When I write JavaScript code, I try to treat objects and arrays as immutable, and I prefer writing pure functions whenever possible. I find this makes code much easier to reason about and avoids lots of subtle bugs. (I learned about the practical benefits of immutable collections from Clojure.)

So it would be nice if Flow could have some sort of immutability checking. With a "const" annotation, the programmer could:

  1. Annotate a variable to indicate it is not supposed to change after initialization
  2. Annotate a function parameter (say an Object or an Array) to guarantee that it won't be modified by the function
  3. Annotate a returned Object to indicate it is not supposed to be altered by the calling code. (This is important for closures or classes that might use the returned Object internally in the future.)

Additionally, a function could have a "pure" annotation to indicate that Flow should check that a function has no side effects. Of course, all functions called by a pure function would need to be checked for purity as well.

Preferably, immutability should apply to nested objects, such as { a : { b : 'c' }}. (In JavaScript, unfortunately, the Object.freeze() function does not prevent nested objects from being modified.)

Flow looks extremely nice, BTW. Thank you for making it open source.

feature request

Most helpful comment

Immutability is now supported!

Please let us know if this does not cover all of your use cases.

If you want to assume that all function arguments are immutable we have an experimental option experimental.const_params. We want to explore making params constant by default and then disable that assumption if you assign to the function argument.

const a: {
  +prop: number
} = {
  prop: 42,
};

const b: $ReadOnlyArray<number> = [1, 2, 3];

const c = Object.freeze({
  prop: 42,
});

a.prop = 1; // Error
b[0] = 2; // Error
c.prop = 3; // Error

All 23 comments

This is a much requested feature. The main issue is syntax: we need to be careful to design this while keeping future versions of JS in mind. No doubt it would be very useful.

The other issue that I see here is that there is no existing standard way of "altering" an immutable object.

var immutable foo = {bar: 'baz'};

foo.bar = 'biz'; // should return a new foo object with a changed property.

The naive way of doing this would be to create a new object with the needed property and add the existing immutable object to its prototype:

function immutableSet(obj, prop, val) {
    var props = {};
    props[prop] = {value: val};
    return Object.create(obj, props);
}

immutableSet(foo, 'bar', 'biz');

It would be nice if flow was able to de-sugar to something like this.

Facebook's immutable.js offers immutable objects:

    var foo1 = Immutable.Map({bar: 'baz'});
    var foo2 = foo1.set('bar', 'biz');
    foo1.get('bar'); // 'baz'
    foo2.get('bar'); // 'biz'

Presumably most of immutable.js's functions could be annotated as pure.

I think the important thing here is the const annotation. Persistent data structures are a whole different topic.

Idea: a contract of immutability for objects and arrays.

E.g.

function foo <T> (x : Immutable<T>) : number {
  x.value = 1; // ERROR: `x` is Immutable<T>
  return x.value;
}

the contract is also considered broken if you call any function that doesn't implement the contract:

function bar <T> (x : T) : number {
  x.value = 1;
  return x.value;
}

function foo <T> (x : Immutable<T>) : number {
  return bar(x); // ERROR: `x` is Immutable<T>, bar expects mutable T
}

function qoo <T> (x : Immutable<T>) : number {
  return x.value;
}

function doo <T> (x : Immutable<T>) : number {
  return qoo(x); // this is OK
}

function too <T> (x : T) : number {
  return x.value;
}

function hoo <T> (x : Immutable<T>) : number {
  return too(x); // this is OK too, b/c can be statically analysed
}

Thank you for reporting this issue and appreciate your patience. We've notified the core team for an update on this issue. We're looking for a response within the next 30 days or the issue may be closed.

I'd say that if starting of scratch the bias here would be to assume all arguments are read-only, since this is the common and safe case, and require annotations when an argument can be mutated .

function append(a: string[], b: string) {
  return a.push(b)
}
==> ERROR: Call to push mutates a read-only argument

function append(a: var string[], b: string) {
  return a.push(b)
}
==> Flow checks sucessfully. 

In any case having a way to express this would be a clear win for type-safety.

I just ran into a related mistake that ideally flow would be able to catch:

function thingy(x) {
  var foo = Immutable.Map({bar: 'baz'});
  if (x) foo.merge({foo: 1});
  return foo;
}

The mistake here is that the line should read return foo.merge, because the .merge function is entirely pure it must be an error to not use the return value in any way.

+1, but -1 to overloading the keyword const.

I like the proposal from @jussi-kalliokoski to solve the syntax issue. Just add a magic type called Immutable or NoMutate or something else. (Immutable is probably not a good idea since Immutable.js exists)

But further, it's very important to add a way to annotate object and class methods as non-mutating.

So that this is no longer a type error:

var map: Immutable.Map<string, number>
var x = map.get('x') && map.get('x') > 10

As of right now, Flow thinks that .get could mutate the object map itself, and so the && doesn't work.

Instead, the workaround for now is:

var map: Immutable.Map<string, number>
var xVal = map.get('x')
var x = xVal && xVal > 10

However, it's not the end of the world to have to define a few extra variables for type-safety. So I support the team focussing on the general use case before tackling these features.

An other use case for Immutabilty would also be to solve type casting errors
like in this example:

/* @flow */
type Foo = { foo: boolean }
type Bar = Foo & { bar: boolean }

const bars: { [key: string]: Bar } = Object.freeze({})
const foos: { [key: string]: Foo } = bars //  generate an error 
/* @flow */
type Foo = { foo: boolean }
type Bar = Foo & { bar: boolean }

const bars: { [key: string]: Bar } = {}
const foos: { [key: string]: Foo } = { ... bars } //  work around
/* @flow */
type Foo = { foo: boolean }
type Bar = Foo & { bar: boolean }

const bars: Array<Bar> = []
const foos: Array<Foo> = bars.slice() //  generate an error 
/* @flow */
type Foo = { foo: boolean }
type Bar = Foo & { bar: boolean }

const bars: Array<Bar> = []
const foos: Array<Foo> = [...bars] //  workaround

The caveat with a special Immutable type (or any way to signify immutability) would be that it only works shallowly.

type ImmutableRecordSomething = $Immutable<{ value: ?string, child: { value: ?string } }>;
const record: ImmutableRecordSomething = {
  value: 'a',
  child: { value: 'b' },
};

record.value = null // Flow error, record is immutable
record.child.value = null // Just fine, only record is protected normally

although that could be solved by doing child: Immutable<{ value: ?string }> I guess.

Another issue would be passing it to other functions. If they are not set up to use flow, they would invalidate the immutability.

type ImmutableRecord = $Immutable<{ value: ?string }>;
const record: ImmutableRecord = { value: 'a' };

record.value = null // Flow error, record is immutable

// Assuming that lodash' mapValues has no flow types and it can't infer immutability
const newRecord: ImmutableRecord = _.mapValues(record, (x) => x);

record.value = null // No error, record is downgraded to mutable because of mapValues

What about creating something similar to inout parameter in swift

  • Function parameters are immutable by default.
  • Trying to change the value of a function parameter from within the body of that function results in a flow error.
  • If you want a function to modify a parameter鈥檚 value, define that parameter as an in-out

In terms of naming, would like to throw another option into the hat $Frozen<type>

eg

$Frozen<{ value: ?string }>
$Frozen<Array<number>>

@cloudkite that could be confusing, or maybe colliding with a possible type for objects that have actually been Object.freeze()-ed

@pgherveou As much as I would love to have explicit mutability like this, I think flow should typecheck on working javascript code, and in javascript you are allowed to mutate your parameters :-/

I know that saying '+1' is extremely annoying, but... +1 here :) I would love this feature.

I wanted to somehow use Immutable Record for this, but it seems it's not very flexible no matter how I try to hack it to work with Flow (no way to capture disjoint unions, for example).

I think $Frozen<T> makes sense as it would just be a way to describe the result of Object.freeze(T), which is already supported by Flow.

Then the definition of Object.freeze could be updated like so:

declare class Object {
    static freeze<T>(o: T): $Frozen<T>;
}

Might try to make a PR for this just to get discussion going and also as a good excuse to learn OCaml 馃槃

This feature basically exists now.

When using an object, you can mark all keys as covariant which makes them read-only. Plus, use an exact type to avoid allowing extra keys as well.

type ImmutablePerson = {|
  +name: string,
  +age: number
|};

As for Arrays, you can use $ReadOnlyArray instead of Array.

There are still edge-cases when dealing with other in-built Objects such as Date, Function etc. but that seems like enough of an edge-case.


I think we should close this issue now.

I think it would still be useful to mark an argument as read-only, as opposed to properties.

@glenjamin That can easily be done with a lint rule.

I was thinking about having a type which is normally mutable, but defining a function which uses that type in a read-only way. I don't think that can be done with per-property covariance or a lint rule?

Immutability is now supported!

Please let us know if this does not cover all of your use cases.

If you want to assume that all function arguments are immutable we have an experimental option experimental.const_params. We want to explore making params constant by default and then disable that assumption if you assign to the function argument.

const a: {
  +prop: number
} = {
  prop: 42,
};

const b: $ReadOnlyArray<number> = [1, 2, 3];

const c = Object.freeze({
  prop: 42,
});

a.prop = 1; // Error
b[0] = 2; // Error
c.prop = 3; // Error

Thanks! This sounds great. Does it enforce the immutability of nested objects and arrays? And is it possible to annotate a function declaration to guarantee that a return value will be constant?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jamesisaac picture jamesisaac  路  44Comments

sophiebits picture sophiebits  路  66Comments

blax picture blax  路  83Comments

STRML picture STRML  路  48Comments

Macil picture Macil  路  186Comments