I'm trying to write a simple function-wrapper around the <button> element.
According to the test-suite:
const ComponentWithChildren: FunctionalComponent<DummerComponentProps> = ({ input, initialInput, children }) => {
return <div>
<span>{initialInput}</span>
<span>{input}</span>
<span>{children}</span>
</div>
}
Am I missing something, or why does this approach not work for me at all?
import { h, FunctionalComponent } from "preact";
export interface ButtonProps {
title: string;
}
export const Button: FunctionalComponent<ButtonProps> = ({ title, children, ...props }) => {
return <button {...props}>{title}</button>;
};
I get a bunch of errors:
Type '(ButtonProps & ComponentProps<FunctionalComponent<ButtonProps>>) | undefined' has no property 'title' and no string index signature.
Type '(ButtonProps & ComponentProps<FunctionalComponent<ButtonProps>>) | undefined' has no property 'children' and no string index signature.
Type '{ key?: any; ref?: ((el: FunctionalComponent<ButtonProps>) => void) | undefined; }' is not assignable to type 'HTMLAttributes'.
Types of property 'ref' are incompatible.
Type '((el: FunctionalComponent<ButtonProps>) => void) | undefined' is not assignable to type '((el?: Element | undefined) => void) | undefined'.
Type '(el: FunctionalComponent<ButtonProps>) => void' is not assignable to type '((el?: Element | undefined) => void) | undefined'.
Type '(el: FunctionalComponent<ButtonProps>) => void' is not assignable to type '(el?: Element | undefined) => void'.
Types of parameters 'el' and 'el' are incompatible.
Type 'Element | undefined' is not assignable to type 'FunctionalComponent<ButtonProps>'.
Type 'undefined' is not assignable to type 'FunctionalComponent<ButtonProps>'.
I tried copy/pasting the entire contents of the test, and it gives similar errors.
I looked at the last successful build from 3 days ago, and the tests didn't look any different.
The most recent change to the types before that was this, I wonder if that broke something?
I'm using Typescript 2.7.2.
Hi @mindplay-dk
Not sure about if this can provide a solution.
buttonProps can be extended from ComponentProps<any> to fit signature. And you still get desired type inference:

Type '{}' is not assignable to type 'ButtonProps & ComponentProps<FunctionalComponent<ButtonProps>>'. Type '{}' is not assignable to type 'ButtonProps'...
Property 'title' is missing in type '{}'
Doesn't seem to help.
import { h, FunctionalComponent, ComponentProps } from "preact";
export interface ButtonProps extends ComponentProps<any> {
title?: string;
}
export const Button: FunctionalComponent<ButtonProps> = ({ title, children, ...props }) => {
return <button {...props}>{title || children}</button>;
};
Still gives me errors:
Type '(ButtonProps & ComponentProps<FunctionalComponent<ButtonProps>>) | undefined' has no property 'title and no string index signature.
Type '(ButtonProps & ComponentProps<FunctionalComponent<ButtonProps>>) | undefined' has no property 'children' and no string index signature.
Also, ComponentProps has been flagged as @deprecated in the master version.
I'm afraid the current tsconfig.json isn't configuring the compiler to strictly check the type-declarations - I just tried building the typescript project with --strict and it's generating a lot of errors:
mindplay@Sidewinder-7240:/mnt/c/workspace/test/preact$ ./node_modules/.bin/tsc -p test/ts/ --strict
test/ts/hoc-test.tsx(18,2): error TS2322: Type 'typeof (Anonymous class)' is not assignable to type 'ComponentConstructor<T & highlightedProps>'.
Types of parameters 'props' and 'props' are incompatible.
Type '(T & highlightedProps) | undefined' is not assignable to type 'T & highlightedProps'.
Type 'undefined' is not assignable to type 'T & highlightedProps'.
Type 'undefined' is not assignable to type 'T'.
test/ts/hoc-test.tsx(39,70): error TS2345: Argument of type 'typeof SimpleComponent' is not assignable to parameter of type 'ComponentFactory<SimpleComponentProps>'.
Type 'typeof SimpleComponent' is not assignable to type 'FunctionalComponent<SimpleComponentProps>'.
Type 'typeof SimpleComponent' provides no match for the signature '(props: RenderableProps<SimpleComponentProps>, context?: any): VNode<any>'.
test/ts/preact.tsx(32,10): error TS2345: Argument of type '({ input, initialInput }: DummerComponentProps) => Element' is not assignable to parameter of type 'string'.
test/ts/preact.tsx(56,10): error TS2564: Property 'refs' has no initializer and is not definitely assigned in the constructor.
I write all of my own projects in strict mode, and would strongly recommend the same here - the typescript tests aren't only interesting to prove that the code can works, but also that the typings are strictly correct, which it looks like they're not.
In my own projects, if a .d.ts fails to compile without warnings, the test has failed.
mm, Im not using scrict mode
BTW I performed the test using these versions
"preact": "~8.2.6",
"typescript": "~2.5.3"
@marvinhagemeister any ideas?
@mindplay-dk The test verify internal typings that are needed to make jsx work. As a user you don't need to be aware of those.
Example:
interface Props {
foo: string;
}
const Whatever = (props: Props) => <div>{props.foo}</div>;
// ...same but written as a standard function
function Whatever(props: Props) {
return <div>{props.foo}</div>
}
// ...same with inline destructuring
const Whatever = ({foo, ...props}: Props) => <div {...props}>{foo}</div>;
@marvinhagemeister the problem is not with component-specific props nor with internal typings.
The problem is with standard JSX props such as onClick and special props like children which are always present - so taking your last example:
const Whatever = ({foo, children, onClick, ...props}: Props) => <div {...props}>{foo}</div>;
Doesn't work.
You example works, but doesn't do what I want - which is to support all standard JSX props (onClick etc.) without having to redeclare those, as well as additional component-specific props.
Your examples only allow foo and none of the standard props.
@mindplay-dk That's quite unusual, but in that case simply extend the same interfaces jsx is using under the hood. They can be found behind the JSX namespace
// Contains all jsx attributes
interface Props extends JSX.DOMAttributes, JSX.HTMLAttributes {
foo: string;
}
const Whatever = ({foo, children, onClick, ...props}: Props) =>
<div {...props}>{foo}</div>;
@marvinhagemeister that's a step in the right direction 馃槃 but still doesn't give me children?
I guess I expected there would be a generic type you could simply extend when you want to pass-through all the general (DOM, HTML and Preact) props?
Is it really "unusual" to extend standard elements like <button> with custom functionality in this way?
What's the alternative?
@mindplay-dk Oh my bad, was typing on a phone when writing the above answer.
interface Props {
foo: string;
}
// RenderableProps includes `children`
const MyComponent = (props: RenderableProps<Props>) => <div>{props.foo}</div>;
// or in most code devs simply specify children themselves
interface Props2 {
foo: string;
children?: ComponentChild;
}
Note that the recent gain in popularity of the render-prop pattern turns the assumption that children are always a component a bit on its head. Take for example the upcoming new context api for react:
<Theme.Consumer>
{theme => <div>Active theme: {theme}</div>}
</Theme.Consumer>
In general you do not want to passthrough all dom/html attributes because that would leak the implementation detail of the underlying tags to the outside.
Take for example a button. Not everything that visually looks like a button should be a <button>-tag. Many designs have buttons disguised as links, where using an <a>-tag is more appropriate. Now why should the user of that component have to care at all how it is rendered under the hood? Because an <a>-tag has different attributes compared to a <button>.
Same story for the onChange prop. The event that is dispatched depends on the underlying dom node and has different properties.
Another point is often overlooked is that the more properties you make public, the more properties you have to support. This makes refactorings a lot more difficult when the props are tied to the underlying implementation. A good rule of thumb is that it is often beneficial to split a component into multiple ones if it has more than around 8 props. More than that usually means that the component does too many things.
They way one extends components in both react and preact is via composition not inheritance. The former is where jsx and the virtual-dom approach really shines.
What's RenderableProps? I don't see that in preact.d.ts.
In general you do not want to passthrough all dom/html attributes because that would leak the implementation detail of the underlying tags to the outside
I understand all of your arguments, and I don't disagree - this just happens to be a different use-case: I'm not building a button component for general purpose, it's purpose-built for buttons in a back-end system with a styleguide for buttons that developers must follow. This component merely add things like an icon-property that implements a button-icon according to our style-guide, but otherwise needs to expose all the options available for the actual button-element.
In a sense, it's just a helper-function that helps build a <button> tag - it's not a "component" in the sense that we need/want to hide any implementation details; it doesn't "replace" the <button> element.
All your points are well taken, I just don't think they apply to this low-level helper function :-)
The RenderableProps type can be found in master it is not released yet on npm. For the time being I'm afraid you have fallback to a custom type like mashing JSX namespace or something similar together or any as a last resort.
I'll close this issue, since the current typings in master solve this issue.
@marvinhagemeister presumably the current typings in master are a work in progress? As mentioned, these aren't strictly correct - I'd highly recommend enabling strict checks for these and failing the build on compilation error. It's really critical that the shipped .d.ts is 100% correct.
@mindplay-dk They indeed are. Enabling strict mode is a great suggestion 馃憤 Happy to review any PRs 馃帀
Had some time to kill and made a PR (#1033)
Agh, sorry about the laugh emoji! It looks like a smiley face! No undo! 馃ぃ
btw looks friendly :smile:
Most helpful comment
Had some time to kill and made a PR (#1033)