flow-runtime icon indicating copy to clipboard operation
flow-runtime copied to clipboard

Support $ReadOnlyArray

Open christophehurpeau opened this issue 6 years ago • 17 comments

Issuehunt badges

This is a:

  • [ ] Bug Report
  • [x] Feature Request
  • [ ] Question
  • [ ] Other

Which concerns:

  • [x] flow-runtime
  • [ ] babel-plugin-flow-runtime
  • [ ] flow-runtime-validators
  • [ ] flow-runtime-mobx
  • [ ] flow-config-parser
  • [ ] The documentation website

type Thing = $ReadOnlyArray<string | number>;

const thing: Thing = [];

console.log(thing);

Output:

Thing must be an object

Expected: {
  [Symbol(Symbol.iterator)]: () => Iterator<T>;
  toLocaleString: () => string;
  concat: <S, Item: $ReadOnlyArray<S> | S> (...items: Array<Item>) => Array<T | S>;
  entries: () => Iterator<[number, T]>;
  every: (callbackfn: (value: T, index: number, array: $ReadOnlyArray<T>) => any, thisArg?: any) => boolean;
  filter: ((callbackfn: Boolean) => Array<$NonMaybeType<T>>) | ((callbackfn: (value: T, index: number, array: $ReadOnlyArray<T>) => any, thisArg?: any) => Array<T>);
  find: (callbackfn: (value: T, index: number, array: $ReadOnlyArray<T>) => any, thisArg?: any) => T | void;
  findIndex: (callbackfn: (value: T, index: number, array: $ReadOnlyArray<T>) => any, thisArg?: any) => number;
  forEach: (callbackfn: (value: T, index: number, array: $ReadOnlyArray<T>) => any, thisArg?: any) => void;
  includes: (searchElement: mixed, fromIndex?: number) => boolean;
  indexOf: (searchElement: mixed, fromIndex?: number) => number;
  join: (separator?: string) => string;
  keys: () => Iterator<number>;
  lastIndexOf: (searchElement: mixed, fromIndex?: number) => number;
  map: <U> (callbackfn: (value: T, index: number, array: $ReadOnlyArray<T>) => U, thisArg?: any) => Array<U>;
  reduce: ((callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: $ReadOnlyArray<T>) => T, initialValue: void) => T) | (<U> (callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: $ReadOnlyArray<T>) => U, initialValue: U) => U);
  reduceRight: ((callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: $ReadOnlyArray<T>) => T, initialValue: void) => T) | (<U> (callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: $ReadOnlyArray<T>) => U, initialValue: U) => U);
  slice: (start?: number, end?: number) => Array<T>;
  some: (callbackfn: (value: T, index: number, array: $ReadOnlyArray<T>) => any, thisArg?: any) => boolean;
  values: () => Iterator<T>;
  length: number;
  [key: number]: T;
}

Actual Value: []

Actual Type: Array<any>

no errors with flow


IssueHunt Summary

Backers (Total: $40.00)

Submitted pull Requests


Become a backer now!

Or submit a pull request to get the deposits!

Tips


IssueHunt has been backed by the following sponsors. Become a sponsor

christophehurpeau avatar Mar 07 '18 10:03 christophehurpeau

@issuehunt has funded $40.00 to this issue.


issuehunt-oss[bot] avatar Jul 24 '19 13:07 issuehunt-oss[bot]

@gajus any ideas how can covariance be supported in general? I can only think of Object.freeze, but not sure if validators are allowed to do something like this to original values

goodmind avatar Jul 24 '19 15:07 goodmind

I don't see a particular reason not to use Object.freeze. Since expectation is to prevent modification of array, this seems like a reasonable solution.

gajus avatar Jul 24 '19 16:07 gajus

@gajus most babel-plugin-flow-runtime assertions throw errors though; I think analogous support for $ReadOnlyArray would wrap the runtime Array in a Proxy that throws if any modification is attempted, rather than just silently preventing modification; do you agree?

EDIT sorry, I forgot that Object.freeze causes errors to be thrown if modification is attempted.

(In any case Object.freeze is better than nothing)

jedwards1211 avatar Jul 25 '19 03:07 jedwards1211

@gajus actually I think there's a problem. Say we pass an array from a context where it's intended to be writable to a context where it's intended to be read-only. Object.freeze would prevent writes in the context where they're intended to work.

Dumb example but:

function logArray(arr: $ReadOnlyArray<number>): Array<number> {
  console.log(arr)
}

const arr: Array<number> = [1, 2, 3]
logArray(arr)
arr.push(4) // whoops, throws if babel-plugin-flow-runtime is applied, due to Object.freeze?
logArray(arr)

jedwards1211 avatar Jul 25 '19 03:07 jedwards1211

I commented in the PR but I think making babel-plugin-flow-runtime wrap the array with a proxy is the only solution that avoids the above problem. A lot more work to wrap the array in all cases $ReadOnlyArray can be used though

jedwards1211 avatar Jul 25 '19 03:07 jedwards1211

@gajus actually I think there's a problem. Say we pass an array from a context where it's intended to be writable to a context where it's intended to be read-only. Object.freeze would prevent writes in the context where they're intended to work.

Why not just make a copy before object freeze?

const newArray = Object.freeze([...input]);

Then the original parameter is not impacted. And since the intention is to have it as read-only, seems reasonable to assume that modifying the array should not affect wherever it came from.

gajus avatar Jul 25 '19 06:07 gajus

Problematic code

const foo: $ReadOnlyArray<number> = [1, 2, 3]
const bar: Array<string> = (foo: any); // bypassing typecheck, but array is still frozen

We can store original array alongside frozen one probably? Or make it work like function or class annotations

goodmind avatar Jul 25 '19 13:07 goodmind

I mean... any is um... you are shooting yourself in the foot anyway.

User can easily work around this with:

const foo: $ReadOnlyArray<number> = [1, 2, 3]
const bar: Array<string> = [
  ...foo
];

gajus avatar Jul 25 '19 13:07 gajus

I thought that any removes any type-assertions, but checked on site, this is not the case with functions or any other type

goodmind avatar Jul 25 '19 13:07 goodmind

I think that supporting any is a separate issue altogether. We could (as you mentioned) keep a copy of whatever the input is and revert it back to the original in case of any. I think that supporting any is pushing it to edge cases though.

gajus avatar Jul 25 '19 13:07 gajus

@gajus there's also warning mode which wouldn't work with Object.freeze, it would always throw

goodmind avatar Jul 25 '19 13:07 goodmind

@gajus there's also warning mode which wouldn't work with Object.freeze, it would always throw

A sensitive thing to do would be to disable this in warning mode.

gajus avatar Jul 25 '19 13:07 gajus

@gajus careful, copying the array causes problems too. $ReadOnlyArray is just a read-only "view", Flow doesn't prevent the view from seeing changes to the array made externally later on, and I don't think it's safe for flow-runtime to do so either. Instead of copying it why not just wrap it in a proxy that blocks write operations?

jedwards1211 avatar Jul 25 '19 14:07 jedwards1211

Instead of copying it why not just wrap it in a proxy that blocks write operations?

My beef with wrapping it in proxy is that (correct me if I am wrong) but the Array.isArray check will fail.

gajus avatar Jul 25 '19 14:07 gajus

That's probably true, actually a problem with proxy or copy approach is that neither is === the original array

jedwards1211 avatar Jul 25 '19 14:07 jedwards1211

I think the only 100% solid approach is for the Babel plugin to add code to any index assignments, length assignments, or function calls on the $ReadOnlyArray identifier

jedwards1211 avatar Jul 25 '19 14:07 jedwards1211