[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
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.
}
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
}
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).
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With