GithubHelp home page GithubHelp logo

Comments (9)

gcanti avatar gcanti commented on September 15, 2024 1

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.

emirotin avatar emirotin commented on September 15, 2024

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:

  1. calling action creators (should be simple in my understanding, they are just functions)
  2. calling bound action creators on connected components (created through react-redux's mapDispatchToProps) — I think I can achieve this by properly annotating the function types in props
  3. selectors (used with reselect) — again they're just functions
  4. props returned from mapStateToProps — as each time it's a function again can I annotate it with function type? But then there's createStructuredSelector
  5. 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.

gcanti avatar gcanti commented on September 15, 2024

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.

emirotin avatar emirotin commented on September 15, 2024

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.

gcanti avatar gcanti commented on September 15, 2024

@emirotin

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.

emirotin avatar emirotin commented on September 15, 2024

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.

gcanti avatar gcanti commented on September 15, 2024

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.

emirotin avatar emirotin commented on September 15, 2024

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.

gcanti avatar gcanti commented on September 15, 2024

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.