Comments (9)
Here's my first proposal (added a requirement: do not change reference if no changes occur)
// @flow
// flow-update.js
export function mergeObject<T: Object>(obj: T, fields: $Shape<T>): T {
const ret = Object.assign({}, obj)
let changed = false
for (const k in fields) {
if (fields.hasOwnProperty(k)) {
const value = fields[k]
changed = changed || obj[k] !== value
ret[k] = value
}
}
return changed ? ret : obj
}
export function mergeArray<T>(arr: Array<T>, indexes: Array<number>, values: Array<T>): Array<T> {
const ret = arr.slice()
let changed = false
for (let i = 0, len = values.length; i < len; i++) {
const index = indexes[i]
const value = values[i]
changed = changed || arr[index] !== value
ret[index] = value
}
return changed ? ret : arr
}
export function swapArray<T>(arr: Array<T>, from: number, to: number): Array<T> {
if (from === to) {
return arr
}
return mergeArray(arr, [from, to], [arr[to], arr[from]])
}
export type Splice<T> = {
start: number,
deleteCount: number,
items?: Array<T>
};
export function spliceArray<T>(arr: Array<T>, splices: Array<Splice<T>>) {
const len = splices.length
if (len === 0) {
return arr
}
const ret = arr.slice()
for (let i = 0; i < len; i++) {
const { start, deleteCount, items = [] } = splices[i]
ret.splice(start, deleteCount, ...items)
}
return ret
}
export function removeDictionary<K, V>(dict: {[key: K]: V}, keys: Array<K>): {[key: K]: V} {
const len = keys.length
if (len === 0) {
return dict
}
const ret: {[key: K]: V} = Object.assign({}, dict)
for (let i = 0; i < len; i++) {
delete ret[keys[i]]
}
return ret
}
Usage
// @flow
import { mergeObject, mergeArray, swapArray, spliceArray, removeDictionary } from './flow-update'
type Person = {
name: string,
age: number,
hobbies?: {
surf: boolean,
climbing: boolean
}
};
const person1: Person = { name: 'Giulio', age: 42 }
console.log(
mergeObject(person1, {
hobbies: mergeObject(person1.hobbies || {}, {
surf: true,
climbing: false
//climbing: 1 // <= error
})
})
)
const arr: Array<number> = [1, 2, 3]
console.log(
mergeArray(arr, [1], [10])
// mergeArray(arr, ['a'], [10]) // <= error
)
console.log(
swapArray(arr, 0, 1)
// swapArray(arr, 0, 'a') // <= error
)
const myFish: Array<string> = ['angel', 'clown', 'mandarin', 'surgeon']
console.log(
spliceArray(myFish, [{ start: 2, deleteCount: 0, items: ['drum'] }])
// spliceArray(myFish, [{ start: 2, deleteCount: 0, items: [1] }]) // <= error
)
console.log(
spliceArray(myFish, [{ start: 3, deleteCount: 2 }])
)
type K = 'a' | 'b';
const dict: {[key: K]: number} = {a: 1, b: 2}
console.log(
removeDictionary(dict, ['a'])
// removeDictionary(dict, ['a', 'c']) // <= error
)
from babel-plugin-tcomb.
I started writing an issue with more places I'd like to see type-safety in my React/Redux code, and to improve my DX by having IDE support, moving it here:
- calling action creators (should be simple in my understanding, they are just functions)
- calling bound action creators on connected components (created through
react-redux
'smapDispatchToProps
) — I think I can achieve this by properly annotating the function types inprops
- selectors (used with
reselect
) — again they're just functions - props returned from
mapStateToProps
— as each time it's a function again can I annotate it with function type? But then there'screateStructuredSelector
- reducer method (actually I have multiple methods, one for each action type)
I'm going to create a simple demo project and add some "virtual" syntax to it to represent a kind of ideal solution I'd like to see. Maybe it'd serve as kind of inspiration.
from babel-plugin-tcomb.
Introduction: the $Shape
magic type
Definition. $Shape<T>
is the (undocumented...) type of the objects which include all or a part of the fields of T
Example
type Person = {
name: string,
age: number
};
({ name: 'Giulio' }: Person): // error
({ name: 'Giulio', age: 42 }: Person); // ok
({ name: 'Giulio' }: $Shape<Person>); // ok
Option 1: Spreading
Note: babel
requires the transform-object-rest-spread
plugin.
type $Strict<T> = T & $Shape<T>;
type Person = {
name: string,
age: number
};
const person1: Person = {
name: 'Giulio',
age: 42 // ok
}
const person2: Person = {
...person1,
age: 'a' // error: string. This type is incompatible with number
}
const person2: Person = {
...person1,
a: 1 // ok (ouch!)
}
const person2: $Strict<Person> = {
...person1,
a: 1 // error: property `a` of object literal. Property not found
}
Nested structures
type Person = {
name: string,
age: number,
hobbies: {
surf: boolean,
climbing: boolean
}
};
const person1: Person = {
name: 'Giulio',
age: 42,
hobbies: {
surf: false,
climbing: true
}
}
const person2: Person = {
...person1,
hobbies: {
...person1.hobbies,
surf: true // ok
}
}
const person2: Person = {
...person1,
hobbies: {
...person1.hobbies,
surf: 'a' // error: This type is incompatible with
}
}
Lists
Push and Unshift
type Person = {
name: string,
age: number,
hobbies: Array<string>
};
const person1: Person = {
name: 'Giulio',
age: 42,
hobbies: ['surf', 'climbing']
}
const person2: Person = {
...person1,
hobbies: [...person1.hobbies, 'tennis'] // ok
}
const person2: Person = {
...person1,
hobbies: ['tennis', ...person1.hobbies] // ok
}
const person2: Person = {
...person1,
hobbies: [...person1.hobbies, 1] // error: number. This type is incompatible with string
}
Replace, slice, splice, swap, etc...
No easy way.
Summary
Pros
- (?)
Cons
- new syntax
- requires a babel plugin
- no easy way to handle lists besides push and unshift
- not exactly readable (?)
from babel-plugin-tcomb.
I like it. The code is less magic, fewer APIs to learn.
The question is: why do you call the syntax new? It's just the most basic ES6 syntax for immutability, it's featured in many Redux tutorials.
I would also argue about its readability, looks just fine for me (but I'm used to ES6, using it every day for about half a year now).
As for replace, slice, splice, swap
it's easy to provide simple mapping functions, like https://gist.github.com/emirotin/8f2e469f3704de09c5b1009240d3535b. Slice and splice are immutable by default. shift
and pop
can be provided as well, trivially implemented with slice
.
from babel-plugin-tcomb.
why do you call the syntax new
IIRC it's a stage-2 proposal, so it's not yet a standard (probably it will though)
As for replace, slice, splice, swap it's easy to provide simple mapping functions
Sure but the point is: if we must go that route for lists (and dictionaries), then why should we adopt a special syntax just for objects? Speaking of which here's another option:
Option 2: specific APIs
The updateObj
API
function updateObj<T: Object>(obj: T, fields: $Shape<T>) : T {
return Object.assign({}, obj, fields)
}
Example
const person1 = { name: 'Giulio', age: 42 }
// here Flow infers that person1 is of type { name: string, age: number }
const person2 = updateObj(person1, { age: 43 } ) // ok
const person2 = updateObj(person1, { a: 1 } ) // error: property `a` of object literal. Property not found
const person2 = updateObj(person1, { age: 'a' } ) // error: string. This type is incompatible with number
const person2 = updateObj(person1, { surname: 'Canti' } ) // error: property `surname` of object literal. Property not found
Nested structures
/*
type Person = {
name: string,
age: number,
hobbies: {
surf: boolean,
climbing: boolean
}
};
*/
const person1 = {
name: 'Giulio',
age: 42,
hobbies: {
surf: false,
climbing: true
}
}
const person2 = updateObj(person1, {
hobbies: updateObj(person1.hobbies, {
surf: true // ok
})
})
const person2 = updateObj(person1, {
hobbies: updateObj(person1.hobbies, {
surf: 'a' // error: string. This type is incompatible with boolean
})
})
Problem. maybe types
type Person = {
name: string,
age: number,
hobbies?: { // <= note the ? here
surf: boolean,
climbing: boolean
}
};
const person1 = { name: 'Giulio', age: 42 }
const person2 = updateObj(person1, { // error: property `hobbies` of object literal. Property not found
hobbies: updateObj(person1.hobbies, {
surf: true,
climbing: false
})
})
In order to tell flow that there is a hobbies
property we must annotate person1
, but then Flow raises another error
const person1: Person = { name: 'Giulio', age: 42 } // <= annotate with the Person type
const person2 = updateObj(person1, { // error: property `surf` of object literal. Property cannot be assigned on possibly undefined value
hobbies: updateObj(person1.hobbies, {
surf: true,
climbing: false
})
})
One option would be to change the updateObj
signature using ?T
instead of T
function updateObj<T: Object>(obj: ?T, fields: $Shape<T>): T {
return obj ?
Object.assign({}, obj, fields) :
fields
}
now it works
const person1: Person = { name: 'Giulio', age: 42 }
const person2 = updateObj(person1, {
hobbies: updateObj(person1.hobbies, {
surf: true,
climbing: false
})
})
const person2 = updateObj(person1, {
hobbies: updateObj(person1.hobbies, {
surf: 'a' // error: This type is incompatible with boolean
climbing: false
})
})
Unfortunately using $Shape
I loose a bit of type safety (seems a known issue with Flow)
const person2 = updateObj(person1, {
hobbies: updateObj(person1.hobbies, {
surf: true
// <= climbing is missing but Flow doesn't raise any error
})
})
from babel-plugin-tcomb.
IIRC it's a stage-2 proposal, so it's not yet a standard (probably it will though)
Stage 2 indeed
if we must take that route for lists (and dictionaries), then why should we adopt a special syntax just for objects
maybe because these methods for lists are proven to be type-safe and are easy to parametrize? While objects can be of arbitrary shape
Unfortunately using $Shape I loose a bit of type safety
Well that makes sense. How would it know if the destination already has the key missing from the patch object? Does Flow analyze the entire program and follow the data flow, or only checks the things at expression boundaries?
from babel-plugin-tcomb.
maybe because these methods for lists are proven to be type-safe and are easy to parametrize
Sorry, perhaps I wasn't clear. I meant: I agree that we should define a series of well-typed functions for lists (we can't do otherwise since spreading doesn't cover all cases), but I propose to define well-typed functions also for objects (and dictionaries). Then, as a user, you are free to use the new syntax (spreading) or a plain old function (updateObj
)
Does Flow analyze the entire program and follow the data flow
Well, actually yes, it's its distinctive feature
or only checks the things at expression boundaries
Even if that were the case, this makes no sense:
type Person = {
name: string,
age: number
};
function foo(x: $Shape<Person>): Person {
return x // <= should raise an error
}
const x: Person = foo({ name: 'Giulio' }) // <= should raise an error
from babel-plugin-tcomb.
Then, as a user, you are free to use the new syntax (spreading) or a plain old function (updateObj)
Oh, of course if we can have both why not doing it :) And if implementing the methods is easier (I assume it is) it's good enough for starters (while I personally would indeed prefer spreading as I find it less verbose).
this makes no sense:
That's true.
from babel-plugin-tcomb.
Ok, I created a new repo for this: https://github.com/gcanti/flow-update
Missing:
- tests
- docs
- perhaps some API (
addDictionary
?)
from babel-plugin-tcomb.
Related Issues (20)
- Refinements example HOT 6
- Typechecking flow's inferred types
- Generator types
- Why generics are not supported
- arguments is not carried across during return type check
- Redux actions with constant string values
- Checking return value of generator function breaks the code HOT 2
- Working with Meteor's new reify HOT 1
- "ReactClass is not defined" error HOT 2
- Cannot set up for ReactNative project HOT 2
- support for argument spread
- add meta object for class?
- Add support for object type spread HOT 11
- tcomb crashes when there are circular "import type" HOT 4
- inspecting function argument and return types
- Adding Custom Error Message to Flow-typed Refinement?
- babel 7 support HOT 3
- ReferenceError: $Values is not defined
- The $Call-way
- React Native: _t is not defined
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from babel-plugin-tcomb.