Skip to content

Instantly share code, notes, and snippets.

@gcanti
Created July 24, 2016 09:12
Show Gist options
  • Select an option

  • Save gcanti/c5d3d73801c780adc0406a6bf4f5bbe5 to your computer and use it in GitHub Desktop.

Select an option

Save gcanti/c5d3d73801c780adc0406a6bf4f5bbe5 to your computer and use it in GitHub Desktop.
function updateObj<T: Object>(obj: T, fields: $Shape<T>) : T {
  return Object.assign({}, obj, fields)
}

const person1 = { name: 'Giulio', age: 42 }
// I guess 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. nullable 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, {
  hobbies: updateObj(person1.hobbies, {
    surf: true,
    climbing: false
  })
})
src/index.js:32
 32:   hobbies: updateObj(person1.hobbies, {
                                           ^ property `climbing` of object literal. Property cannot be assigned on possibly undefined value
 32:   hobbies: updateObj(person1.hobbies, {
                          ^^^^^^^^^^^^^^^ undefined

src/index.js:32
 32:   hobbies: updateObj(person1.hobbies, {
                                           ^ property `surf` of object literal. Property cannot be assigned on possibly undefined value
 32:   hobbies: updateObj(person1.hobbies, {
                          ^^^^^^^^^^^^^^^ undefined

One option would be to change the updateObj signature using ?T instead of T and returning fields when obj is nully

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
  })
})

but 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
  })
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment