Perspective ComponentMeta Typing Issue

I feel like I am missing the smallest thing here, however for some reason when I am trying to build this very simple button component, it is giving me an error about the Component class not being assignable to type PComponent (Both things inside the @inductiveautomation/perspective-client library that I have not messed with in my install.

Am I missing something here?

src/components/button.tsx

/**
 * Example of a component which displays an Button, given a URL.
 */
import * as React from 'react';
import {
	Component,
	ComponentMeta,
	ComponentProps,
	PComponent,
	PropertyTree,
	SizeObject
} from '@inductiveautomation/perspective-client';

export const COMPONENT_TYPE = "example.input.button";

export interface ButtonProps {
	text: string;   
}

export class Button extends Component<ComponentProps<ButtonProps>, any> {
	render() {
		const { props: { text }, emit } = this.props;
		return (
			<div { ...emit() }>
				{text}
			</div>
		);
	}
}

export class ButtonMeta implements ComponentMeta {

	getComponentType(): string {
		return COMPONENT_TYPE;
	}

	getViewComponent(): PComponent {
		return Button;
	}

	getDefaultSize(): SizeObject {
		return ({
			width: 360,
			height: 360
		});
	}

	getPropsReducer(tree: PropertyTree): ButtonProps {
		return {
			text: tree.readString("text", "")
		};
	}
}

package.json

{
	"name": "@me/dumb-button",
	"version": "0.0.1",
	"description": "A stupid button that won't compile",
	"main": "dist/index.js",
	"types": "dist/index.d.ts",
	"scripts": {
		"build": "webpack --mode production"
	},
	"dependencies": {
		"@inductiveautomation/perspective-client": "2.1.34",
		"axios": "^0.28.0",
		"react": "16.14.0",
		"react-dom": "16.14.0"
	},
	"devDependencies": {
		"@types/react": "16.8.8",
		"@types/react-dom": "16.8.3",
		"css-loader": "^6.1.2",
		"mini-css-extract-plugin": "^2.6.0",
		"ts-loader": "^9.2.9",
		"typescript": "4.2.3",
		"webpack": "^5.93.0",
		"webpack-cli": "4.9.2"
	}
}

tsconfig.json

{
	"compilerOptions": {
		"allowJs": false,
		"allowSyntheticDefaultImports": true,
		"baseUrl": ".",
		"paths": {
			"*": [
				"*"
			]
		},
		"jsx": "react",
		"module": "commonjs",
		"noEmit": false,
		"outDir": "dist/",
		"pretty": true,
		"sourceMap": true,
		"strict": true,
		"target": "es6",
		"lib": [
			"es6",
			"dom",
			"es7",
			"es2017"
		],
		"forceConsistentCasingInFileNames": true,
		"types": [
			"react",
			"react-dom"
		]
	},
	"include": [
		"./src/**/*.ts",
		"./src/**/*.tsx"
	],
	"exclude": [
		"node_modules",
		"dist"
	]
}

The error itself:

ERROR in /Users/kgamble/Work/Ignition/modules/My Modules/dumb-button/web/src/components/Button.tsx
./src/components/Button.tsx 53:2-16
[tsl] ERROR in /Users/kgamble/Work/Ignition/modules/My Modules/dumb-button/web/src/components/Button.tsx(53,3)
      TS2322: Type 'typeof Button' is not assignable to type 'PComponent'.
  Type 'typeof Button' is not assignable to type 'ComponentClass<ComponentProps<PlainObject, PlainObject>, any>'.
    Types of parameters 'props' and 'props' are incompatible.
      Type 'ComponentProps<PlainObject, PlainObject>' is not assignable to type 'Readonly<ComponentProps<ButtonProps, PlainObject>>'.
        Types of property 'props' are incompatible.
          Property 'text' is missing in type 'PlainObject' but required in type 'ButtonProps'.

I appreciate any advice!

I have found that I have to cast my component as PComponent in getViewComponent to make Typescript happy, this issue might be the same?

getViewComponent(): PComponent {
        return ChartjsComponent as PComponent
    }

Unfortunately I tried this as well and no luck

getViewComponent(): PComponent {
	return Button as PComponent;
}

Error:

Conversion of type 'typeof Button' to type 'PComponent' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
  Type 'typeof Button' is not comparable to type 'ComponentClass<ComponentProps<PlainObject, PlainObject>, any>'.
    Types of parameters 'props' and 'props' are incompatible.
      Type 'ComponentProps<PlainObject, PlainObject>' is not comparable to type 'Readonly<ComponentProps<ButtonProps, PlainObject>>'.
        Types of property 'props' are incompatible.
          Property 'text' is missing in type 'PlainObject' but required in type 'ButtonProps'.ts(2352)

Hm, I’m not sure. Maybe someone experienced with class based components can chime in, I’ve only used the functional style.

Not sure if it’s useful but this is my “starter” component.


import * as React from 'react'
import {
    ComponentMeta,
    ComponentProps,
    PComponent,
    PropertyTree,
    SizeObject,
} from '@inductiveautomation/perspective-client'
import { useState } from 'react'

export const COMPONENT_TYPE = 'mussonindustrial.display.example'

interface ExampleComponentProps {
    text?: string
    value?: number
}

export function ExampleComponent(props: ComponentProps<ExampleComponentProps>) {
    const [counter, setCounter] = useState(props.props.value)
    const handleClick = () => {
        if (counter != undefined) {
            setCounter(counter + 1)
        } else {
            setCounter(0)
        }
        props.store.props.write('value', counter)
    }

    return (
        <div {...props.emit({ classes: ['examplecomponent'] })}>
            <span>This is a simple component, with only a "text" prop: </span>
            <span>{props.props.text}</span>
            <span>{props.props.value}</span>
            <button onClick={handleClick}>Increment</button>
        </div>
    )
}

export class ExampleComponentMeta implements ComponentMeta {
    getComponentType(): string {
        return COMPONENT_TYPE
    }

    getDefaultSize(): SizeObject {
        return {
            width: 160,
            height: 64,
        }
    }

    getPropsReducer(tree: PropertyTree): ExampleComponentProps {
        return {
            text: tree.readString('text', 'Default Text!'),
            value: tree.readNumber('value', 0),
        }
    }

    getViewComponent(): PComponent {
        return ExampleComponent
    }
}

1 Like

Weirdly even if I switch to a function I am getting the same error...

src/components/button.tsx

/**
 * Example of a component which displays an Button, given a URL.
 */
import * as React from 'react';
import {
	Component,
	ComponentMeta,
	ComponentProps,
	PComponent,
	PropertyTree,
	SizeObject
} from '@inductiveautomation/perspective-client';
import { useState } from 'react'


export const COMPONENT_TYPE = "example.input.button";

export interface ButtonProps {
	text: string;   
}

export function ButtonComponent(props: ComponentProps<ButtonProps>) {
    const [text] = useState(props.props.text)
	
	return (
		<div { ...props.emit() }>
			{text}
		</div>
	);
}

export class ButtonMeta implements ComponentMeta {

	getComponentType(): string {
		return COMPONENT_TYPE;
	}

	getViewComponent(): PComponent {
		return ButtonComponent;
	}

	getDefaultSize(): SizeObject {
		return ({
			width: 360,
			height: 360
		});
	}

	getPropsReducer(tree: PropertyTree): ButtonProps {
		return {
			text: tree.readString("text", "")
		};
	}
}

error

Type '(props: ComponentProps<ButtonProps, PlainObject>) => Element' is not assignable to type 'PComponent'.
  Type '(props: ComponentProps<ButtonProps, PlainObject>) => Element' is not assignable to type 'FunctionComponent<ComponentProps<PlainObject, PlainObject>>'.
    Types of parameters 'props' and 'props' are incompatible.
      Type 'PropsWithChildren<ComponentProps<PlainObject, PlainObject>>' is not assignable to type 'ComponentProps<ButtonProps, PlainObject>'.
        Types of property 'props' are incompatible.
          Property 'text' is missing in type 'PlainObject' but required in type 'ButtonProps'.ts(2322)

I figured it out! (I think)

I needed to make my component props optional?

export interface ButtonProps {
	text?: string;   
}

I am not really sure why I needed to do this.

Also, @bmusson out of curiosity, why use the functional style over class based? any reason or just happens to be what you did?

Functional components seem to be the newest paradigm for React, and since I had no previous experience it made sense to only focus on the current best-practice (I don’t want to be a web developer, learning React is just a means to an end :laughing:).

4 Likes

Your functional example builds on my machine, with the addition of

getViewComponent(): PComponent {
+	return ButtonComponent as PComponent;
}

I can't convince Typescript to be happy with the class-based version at all :man_shrugging:

getViewComponent(): PComponent {
+	return Button as any;
}

3 Likes