Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript typing issue (marginally related to (P)React)

[Edit: I've simplified my original question]

Let's assume I want to define UI components in exactly the following way (the following lines shall not be changed in any way - any solution that will change the following lines is frankly not a solution I am looking for ... for example just writing render({ name: 'World' }) is not an option ... neither is the non-null assertion operator ...neither using currying or a builder pattern or something like a withDefaultProps helper function ... etc. ... these are just workarounds (yet easily working) for the actual problem below):

// please do not change anything in this code snippet

type HelloWorldProps = {
  name?: string
}

export default component<HelloWorldProps>({
  displayName: 'HelloWorld',
  defaultProps: { name: 'World' },

  render(props) {
    // the next line shall NOT throw a compile error
    // that props.name might be undefined
    return `HELLO ${props.name.toUpperCase()}`

    // [Edit] Please ignore that the function returns a string
    // and not a virtual element or whatever - this is not important here.
    // My question is about a TypeScript-only problem,
    // not about a React problem.

    // [Edit] As it has been caused some misunderstanding:
    // The type of argument `props` in the render function shall
    // basically be the original component props type plus (&) all
    // properties that are given in `defaultProps` shall be required now.
    // Those optional props that have no default value shall still
    // be optional. If ComponentType is the original type of the component
    // properties and the type of the `defaultProps` is D then
    // the type of the first argument in the render function shall
    // be: ComponentProps & D

     // [Edit] As it seems not to be 100% clear what I am looking for:
     // The problem is mainly because the function "component" depends basically
     // on two types: One is the type of the component props the other is
     // is the type of the default props. AFAIK it's currently only possible in
     // TypeScript to infer either both of them or none of them (or use
     // default types for the type variables - which is not very useful here
     // as the defaults are {}). But I only want to declare ONE type
     // (HelloWorldProps).
     // All workarounds that I know of are either to explictly declare both
     // types or split the single function "component" into two or more
     // functions - then you do not have that problem any more,
     // but then you have to change the syntax and that is exactly
     // what I do NOT want to do (a fact that is the most important
     // part of the  whole question):

     // [this is not the solution I am looking for]
     // export default component<HelloWorldProps>('HelloWorld')({
     //   defaultProps: {...},
     //   render(props) {...}
     // })

     // [this is not the solution I am looking for]
     // export default component<HelloWorldProps>('HelloWorld')
     //   .defaultProps({...})
     //   .render(props => ...) // `render` is the function component
     //                         // creator is this builder pattern

     // [this is not the solution I am looking for]
     // export default component<HelloWorldProps>({
     //   displayName: 'HelloWorld',
     //   render: withDefaultProps(defaultProps, props => { ... })
     // })

     // [this is not the solution I am looking for]
     // type HelloWorldProps = {...}
     // const defaultProps: Partial<HelloWorldProps> = {...}
     // export default component<HelloWorldProps, typeof defaultProps>({...})

     // [this is not the solution I am looking for]
     // return `HELLO ${props.name!.toUpperCase()}`

     // [this is not the solution I am looking for]
     // render(props: HelloWorldProps & typeof defaultProps) {...}   

     // [this is not the solution I am looking for]
     // render({ name = 'HelloWorld' }) {...}
  }
})

How exactly do I have to type the function component and the type ComponentConfig to make the above code work properly?

function component<...>(config: ComponentConfig<...>): any {
  ...
}

Please find a non-working (!) demo here:

» DEMO

[Edit] Maybe this just not possible at the moment. I think it should be possible if this feature would be implemented for the TS compiler. https://github.com/Microsoft/TypeScript/issues/16597

like image 537
Natasha Avatar asked Aug 23 '19 09:08

Natasha


2 Answers

After some days discusses and researches, it's not possible to solve your problem given your restrictions.

As you point in your question:

[Edit] Maybe this just not possible at the moment. I think it should be possible if this feature would be implemented for the TS compiler. https://github.com/Microsoft/TypeScript/issues/16597

TS won't infer generics at the moment of the function/class declaration. The idea of your issue is the same as for issue 16597:

// issue example
class Greeter<T, S> {
    greeting: T;
    constructor(message: T, message2: S) {
        this.greeting = message;
    }

}

// your issue
function component<P extends {} = {}>(config: ComponentConfig<P>): any {
  return null
}

// generalizing
const function<SOME_GIVEN_TYPE, TYPE_TO_BE_INFERED?>() {
  // TYPE_TO_BE_INFERED is defined inside de function/class.
}
like image 58
Pedro Arantes Avatar answered Nov 06 '22 11:11

Pedro Arantes


The reason there's a compile error on your code is because, indeed, props.name could be undefined.

To fix it you can simply change the type declaration from

type GreeterProps = {
  name?: string // the ? after name means name must be undefined OR string
}

to

type GreeterProps = {
  name: string // name must be a string
}

What if you really want props.name to be able to be left undefined?

You could just change the logic inside render, an example would be:

render(props) {
  if (this.props.name === undefined) return 'You don\'t have a name =/';
  return 'HELLO ' + props.name.toUpperCase();
}
Why you would need to do that?

The answer is very simple, if props.name can be undefined you'd just call .toUpperCase on undefined. Test on your console what happens if you do (PS.: On a real app the result would be even messier).

Further notes

By the way, on a typical TypeScript + React App you'd declare default props using

  public static defaultProps = {
    ...
  };

instead of the approach you've used.

like image 2
LuDanin Avatar answered Nov 06 '22 10:11

LuDanin