Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
430 views
in Technique[技术] by (71.8m points)

reactjs - Type Property Relying on Return Type of Another Property

I'm trying to pass an object to a function (in this case props to a React component).

This object contains the following properties:

  • data - some arbitrary data
  • selector - function which will return some part of the data
  • render - function which will handle rendering the selected data (JSX)

I'm unsure how to type this out correctly.

I initially assumed it could be done this way:

type Props<D, S> = {
  data: D
  selector: (data: D) => S
  render: (data: S) => any
}

const Component = <D, S>(props: Props<D, S>) => null

Component({
  data: { test: true },
  selector: (data) => data.test,
  render: (test) => test, // test is unknown
})

This results in the generic, S, being unknown. However if we remove the render property which relies on S we get the correct return type (boolean).

I've also tried:

  • Using a generic parameter, S extends (data: D) => unknown, with render's data type being ReturnType<S>.
  • Using a separate type to infer and extract the selector's return type using type Extract<D, S> = S extends (data: D) => infer T ? T : D.
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

You've run into a design limitation of TypeScript. See microsoft/TypeScript#38872 for more information.

The problem is that you gave selector and render property callbacks whose parameters (data and test) had no explicit type annotation. Thus they are contextually typed; the compiler needs to infer types for these parameters and cannot use them directly to infer other types. The compiler holds off on this and tries to infer D and S from what it currently knows. It can infer D as {test: boolean} because the data property is of this type. But it has no idea what to infer S as, and it defaults to unknown. At this point the compiler can begin to perform contextual typing: the data parameter of the selector callback is now known to be of type {test: boolean}, but the test parameter of the render callback is given the type unknown. At this point, type inference ends, and you're stuck.

According to a comment by the lead architect of TypeScript:

In order to support this particular scenario we'd need additional inference phases, i.e. one per contextually sensitive property value, similar to what we do for multiple contextually sensitive parameters. Not clear that we want to venture there, and it would still be sensitive to the order in which object literal members are written. Ultimately there are limits to what we can do without full unification in type inference.


So what can be done? The problem is the interplay between contextual callback parameter types and generic parameter inference. If you're willing to give one of those up, you can cut the Gordian Knot of type inference. For example, you can give up some contextual callback parameter typing by manually annotating some callback parameter types:

Component({
  data: { test: true },
  selector: (data: { test: boolean }) => data.test, // annotate here
  render: (test) => test, // test is boolean
})

Or, you can give up on generic parameter type inference by manually specifying your generic type parameters:

Component<{ test: boolean }, boolean>({ // specify here
  data: { test: true },
  selector: (data) => data.test,
  render: (test) => test, // test is boolean
})

Or, if you're not willing to do that, maybe you can create your Props<D, S> values in stages where each stage only requires a little bit of type inference. For example, you can replace property values with function parameters (see above quote "similar to what we do for multiple contextually sensitive parameters"):

const makeProps = <D, S>(
  data: D, selector: (data: D) => S, render: (data: S) => any
): Props<D, S> => ({ data, selector, render });

Component(makeProps(
  { test: true },
  (data) => data.test,
  (test) => test // boolean
));

Or, more verbosely but possibly more understandable, use afluent builder pattern:

const PropsBuilder = {
  data: <D,>(data: D) => ({
    selector: <S,>(selector: (data: D) => S) => ({
      render: (render: (data: S) => any): Props<D, S> => ({
        data, selector, render
      })
    })
  })
}

Component(PropsBuilder
  .data({ test: true })
  .selector((data) => data.test)
  .render((test) => test) // boolean
);

I tend to prefer builder patterns in cases where TypeScript's type inference capabilities fall short, but it's up to you.

Playground link to code


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...