epitath icon indicating copy to clipboard operation
epitath copied to clipboard

Typescript support

Open aleclarson opened this issue 7 years ago • 5 comments
trafficstars

I've been looking into adding Typescript support to this library. Here are the issues I encountered.

  1. JSX factory invocations (eg: <Foo />) return ReactElement<any> (but a fix is on the way), which means we can't infer the prop types of any returned elements

  2. @types/react does not allow the children prop to be a function (at least with "stateless components") This is only true if you explicitly type your stateless component using React.SFC

  3. Typescript cannot infer what yield returns, which means render props are untyped by default https://github.com/Microsoft/TypeScript/issues/26959

Now I'm wondering if "yield to wrap the next yield or return" is worth the lack of type safety, or if I should bite the bullet on using render props without this library. 😞

If generators were always immutable in Javascript, this would be a lot easier.

aleclarson avatar Sep 20 '18 19:09 aleclarson

The best workaround is something like:

import { Component, ReactNode } from 'react'

interface ParentProps<T extends object = any> {
  children?: ReactNode[] | ((props: T) => ReactNode)
}

// Infer the argument types of a function
type In<T> = T extends (...args: infer U) => any ? U : []

/** The functional "render children" prop */
type ChildFunction<T extends ParentProps> = Exclude<
  T['children'],
  ReactNode[] | undefined
>

// Component with render props
type RPC = Component<ParentProps> | ((props: ParentProps) => ReactNode)

/** Extract the render props of a component */
export type RenderProps<T extends RPC> = In<
  ChildFunction<T extends Component<ParentProps> ? T['props'] : In<T>[0]>
>[0]

And then:

import { Component, ReactNode } from 'react'
import epitath, { RenderProps } from 'epitath'

declare const Foo: (props: FooProps) => ReactNode
declare type FooProps = {
  children?: (value: { a: number }) => ReactNode
}

declare class Bar extends Component<BarProps> {}
declare type BarProps = {
  children?: (value: { b: number }) => ReactNode
}

const App = epitath(function* () {
  const { a }: RenderProps<typeof Foo> = yield <Foo />
  const { b }: RenderProps<Bar> = yield <Bar />
  return a + b
})

Example: https://codesandbox.io/s/qqonn05154?view=editor (note: CodeSandbox is using TS 2.7 so not exactly a working example right now)

aleclarson avatar Sep 20 '18 20:09 aleclarson

That is interesting! I don't know much about TS myself, but I'm going to read more about those and see if we can ship support right away

fakenickels avatar Sep 21 '18 15:09 fakenickels

Nice. Also, I opened https://github.com/Microsoft/TypeScript/issues/27267 to see if better support for immutable generators could be added.

aleclarson avatar Sep 21 '18 15:09 aleclarson

Just to share my config which works so-and-so (having typing issues with yielded elements not having an explicit children prop)

declare module "epitath" {
    import * as React from "react"

    const epitath: <P extends {}>(
        Component: (props: P) => IterableIterator<React.ReactElement<any>>
    ) => React.ComponentType<P>

    export default epitath
}

// interface Props { foo: string }
// Usage: const FooComponent = epitath<Props>(function* ({ foo }) { ... })

williamboman avatar Oct 26 '18 09:10 williamboman

FYI meanwhile we discuss about this, ReasonML implementation of Epitath gave free type inference support https://medium.com/astrocoders/render-props-composition-for-reasonml-is-here-b9c004ca9fcb

fakenickels avatar Oct 30 '18 13:10 fakenickels