facebook/flow

Exact type props and spreading passthrough props

Ricardo-Marques opened this issue ยท 13 comments

When typing the props on a component that accepts some passthrough props to give to its child, I'd expect this to work

import * as React from 'react';

type FirstComponentProps = {|
	a: number,
	b: number       
|}

class FirstComponent extends React.Component<FirstComponentProps> {}

type SecondComponentOwnProps = {|
	c: number
|}

type SecondComponentProps = {|
	...FirstComponentProps,
	...SecondComponentOwnProps
|}

class MySecondComponent extends React.Component<SecondComponentProps> {
	render () {
      	        const { c, ...props } = this.props 
    	        return <FirstComponent {...props} />
	}
}

const Node = <MySecondComponent a={1} b={2} c={3} />

But it errors with props of React element 'FirstComponent'. Inexact type is incompatible with exact type

https://flow.org/try/#0JYWwDg9gTgLgBAKjgQwM5wEoFNkGN4BmUEIcA5FDvmQNwBQdMAnmFnAGLBSowDCJkAHZZBMAArEw6ALxwA3gB86ASGQAuOIICuIAEZYoAGhW6N2vQbhXrcOgoC+DXABs06Ttz4CIw0XCwAHjAiACbo2HgwAHT84D4iMAA8HjyxQgkSEFIAfPKOjCxsAMpYuD4hafGiAPIA7oKZUnCyiiq4Zjr6UHb5zKxwJWWCFd6+4pIy8krKUbMpXnFjjajGM7OD5ZVjdQ0TPU6uqOgAskwbw1sJ-kGh4VTRl6KJ5yOLGRO5ciqUw5YAFABKeR0GxWZRDHjyOC4QxwWZRMATOD2ZpwGAAC2AqARSJB1mUlBgWiggjgyS4qVGVzk8MRWVQKIA9NkVI58hD4AA5CAhNiyRKnF6PeDIaRyACMKN0YoATCjcGKAMxMllAA

Passing each prop individually works

class MySecondComponent extends React.Component<SecondComponentProps> {
	render () {
    	        return <FirstComponent a={this.props.a} b={this.props.b} />
	}
}

But this does not seem like it would be the best solution.

Also attempted this to no avail.

class MySecondComponent extends React.Component<SecondComponentProps> {
	render () {
      	        const { c } = this.props
      	        const passThrough = getRest(this.props, { c })
      	        return <FirstComponent {...passThrough} />
	}
}

function getRest<Props: {}, ToOmit: {}> (
  props: Props, toOmit: ToOmit
): $Rest<Props, ToOmit> {
	const rest = { ...props }
	Object.keys(toOmit).forEach(prop => delete rest[prop])
	return rest
}

https://flow.org/try/#0JYWwDg9gTgLgBAKjgQwM5wEoFNkGN4BmUEIcA5FDvmQNwBQdMAnmFnAGLBSowDCJkAHZZBMAArEw6ALxwA3gB86ASGQAuOIICuIAEZYoAGhW6N2vQbhXrcOgoC+DXABs06Ttz4CIw0XCwAHjAiACbo2HgwAHT84D4iMAA8HjyxQgkSEFIAfPKOjCxsAMpYuD4hafGiAPIA7oKZUnCyiiq4Zjr6UHb5zKxwJWWCFd6+4pIy8krKUbMpXnFjjajGM7OD5ZVjdQ0TPU6uqOgAskwbw1sJ-kGh4VTRl6KJ5yOLGRO5ciqUw5YAFABKeR0GxWZRDHjyOC4OD2ZpwGAAC2AqCiYD2oOsEPgYDcABVEcQtABzRHw4lYGDYHh-JEotETQxQmH2AEg6zKSgwLRQQRwZJcVKjK5yWZo-GEiAkxFwgD02RUjny2LgADkICE2LJEqcXo94MhpHIAIxw3RGgBMcNwRoAzHKFXQCFpBPhgD44BSqVgeIllho5PYmXiINUQMAYAH7Lk-uz0VlUBplkyYKHw5G4CGwxG6ACNAASalJZOZtMRz5tHyQyiQlpwMXxpqOZTVXQAK1K0QA1lgmKhaWWYACogRoABRPCIv6N5q5TXOSlsGswADajYAumzOZSeXzl3R7EA

The spread operator creates an unsealed object type which is incompatible with an exact type. Pass props explicitly (as you've noted) or use inexact types.

I see. How does that affect spreading props? I see that this still errors.

class MySecondComponent extends React.Component<SecondComponentProps> {
	render () {
    	        return <FirstComponent {...getFirstComponentProps(this.props)} />
	}
}

function getFirstComponentProps (props): FirstComponentProps {
      return {
            a: props.a,
            b: props.b
      }
}

Really seems like this should work. It would allow the definition for the function that extracts the props the component cares about (getFirstComponentProps) to live next to the corresponding component (FirstComponent), rather than having to rely on passing each prop individually or using non-exact types.

Flow seems to have issues inferring that the result of a spread operation is exact (#2405). A temporary solution is to use the shape of FirstComponentProps:

- class FirstComponent extends React.Component<FirstComponentProps> {}
+ class FirstComponent extends React.Component<$Shape<FirstComponentProps>> {}

Good enough for me, thanks!

Edit: Unfortunately this solution does not work when no props are given.

class MySecondComponent extends React.Component<$Shape<SecondComponentProps>> {
	render () {
                // should error but doesn't
    	        return <FirstComponent />
	}
}

Are exact types a requirement? If spread props are handled explicitly within components, inexact types could be sufficient:

type FooProps = {
  a: number,
  b: number
};

class A extends React.Component<FooProps> {
  render() {
    return <div data-a={this.props.a} data-b={this.props.b} />;
  }
};

type BarProps = {
  c: number
};

type BazProps = FooProps & BarProps;

class B extends React.Component<BazProps> {
  render() {
    return <A {...this.props} />
  }
}
    
<B a={1} b={2} c={3} />
<B a={1} b={2} c={3} d={4} />

Yes, exact types are worth explicitly passing each prop to the component in my opinion.
I am currently building a UI component library to be used by my entire team and getting errors in real time for mistyped prop names is invaluable.

The spread operator creates an unsealed object type which is incompatible with an exact type. Pass props explicitly (as you've noted) or use inexact types.

Meaning, that this will never work in flow (and there is no intension to support it/it is not possible for a reason)?

Code

type A = {| a: number, b: string |};

function f(x: A): A {
  return { ...x };
}

p.s.: the issue is about using exact object types with spread operator, so i made a minimum demo:

/cc @idiostruct

Your minimum demo will work if the object returned by f is frozen:

type A = {| a: number, b: string |};

function f1(x: A): A {
  // `o` can be mutated making it incompatible with an exact type.
  const o = { ...x };
  o.c = 3;
  return o;
}

function f2(x: A): A {
  // `o` cannot be mutated making it compatible with an exact type.
  const o = Object.freeze({ ...x });
  return o;
}

The spread operator creates an unsealed object type which is incompatible with an exact type. Pass props explicitly (as you've noted) or use inexact types.

What's the rationale for this? <div {...props}/>; compiles to React.createElement("div", props); so it's not like a copy of props is actually being created. Also, I would expect that if obj is sealed that {...obj} would also be sealed.

Given the release of Flow 70 with support for object-rest-spread maintaining a seal, I would expect the same to work with jsx-rest-spread. However, it appears not to (even with Object.freeze backflips).

๐Ÿ˜ž

Using React.createElement directly is a workaround - thanks for the tip @kevinbarabash .

@rattrayalex-stripe I'm glad to hear that object-rest-spread maintains sealed objects in the latest version. Hopefully it won't too long before jsx-rest-spread does the same.

Indeed, that is great news! If anyone in this thread happens to find that it has been released please do share!

Seems like this is resolved in the latest 0.71 release!! Closing this issue, thanks everyone!