jsx-vue2
jsx-vue2 copied to clipboard
Functional Components with TypeScript
HI!✌
I have 2 components. The first looks like this:
import { RenderContext, VNode } from 'vue';
import './BaseTitle.sass';
export interface IBaseTitleProps {
level: number;
}
export const BaseTitle = (context: RenderContext<IBaseTitleProps>): VNode => {
const { level } = context.props;
const HeadingComponent = `h${level}`;
return (
<HeadingComponent
class={`title base-title base-title_level-${level} ${context.data.staticClass || ''} ${context.data.class ||
''}`}>
{context.children}
</HeadingComponent>
);
};
And the second one:
import { VueComponent } from 'types/vue-components';
import { Component } from 'nuxt-property-decorator';
import { VNode } from 'vue';
import { BaseTitle } from 'components/base/BaseTitle';
@Component({
name: 'TheHeader',
})
export default class TheHeader extends VueComponent {
public render(): VNode {
return (
<header class='page-header'>
<BaseTitle level={4}>Page title</BaseTitle>
</header>
);
}
}
I get an error when I pass the prop level
there.
TS2322: Type '{ level: number; }' is not assignable to type 'RenderContext<IBaseTitleProps>'. Property 'level' does not exist on type 'RenderContext<IBaseTitleProps>'.
RenderContext
interface.
export interface RenderContext<Props=DefaultProps> {
props: Props;
children: VNode[];
slots(): any;
data: VNodeData;
parent: Vue;
listeners: { [key: string]: Function | Function[] };
scopedSlots: { [key: string]: NormalizedScopedSlot };
injections: any
}
I can rewrite it to something like this
<BaseTitle
props={{
level: 4,
}}
/>
but it also requires me to pass all other fields from RenderContext
interface
How to properly use functional components and TypeScript? Is there any example? Thanks.
I have the same questions.Maybe we cannot't use tsx to write a react like Functional Component ,beacuse vue FC's parameter is a context ,not a props. i juse remove the RenderContext type constraints for the context and write props type myself.
I have a working solution for this, but it's neither pretty nor technically correct :cry:
import { RenderContext } from 'vue'
type Maybe<T> = T | undefined | null
type TsxComponent<Props> = (
args: Partial<RenderContext<Props>> & {
[k in keyof Props]: Maybe<Props[k]>
}
) => VueTsxSupport.JSX.Element // or whatever you're using for JSX/TSX
interface LabelInputProps {
name: string
type: string
}
const LabeledInput: TsxComponent<LabelInputProps> = ({props}) => {
return (
<div>
<p>{props.name}</p>
<input type={props.type} />
</div>
)
}
A warning about this though:
This works through a hack, by copying the properties of the type you pass into TsxComponent<Props>
into the argument definitions at the TOP LEVEL. Because the TSX Element seems to autocomplete the parameters as all top-level arguments instead of just props
.
RenderContext<Props>
adds the types to the { props }
definitions, and then [k in keyof Props]: Maybe<T[Props]>
adds them to the top-level as well so that they appear as autocomplete options in the TSX Element.
This makes them incorrectly appear as argument values outside of props
when destructuring in the function parameters too.
If anyone knows how to make this type so that it shows up on the TSX element autocomplete but not in the top-level function params, please post ideas. I think this may not be possible since they have to share a single type definition.
I decided to refuse such a notation of functional components, since I could not find an elegant solution. I Did everything through Vue.extend()
. Now my component looks like this:
import Vue, { RenderContext, VNode, CreateElement, PropType } from 'vue';
import './BaseTitle.sass';
export interface IBaseTitleProps {
level: number;
}
export const BaseTitle = Vue.extend({
functional: true,
props: {
level: {
type: Number as PropType<IBaseTitleProps['level']>,
default: 1,
required: true,
},
},
render(_h: CreateElement, ctx: RenderContext<IBaseTitleProps>): VNode {
const { staticClass, class: cls } = ctx.data;
const { level } = ctx.props;
const HeadingComponent = `h${level}`;
return (
<HeadingComponent class={`title base-title base-title_level-${level} ${staticClass || ''} ${cls || ''}`}>
{ctx.children}
</HeadingComponent>
);
},
});
Autocomplete for props also works fine in latest WebStorm 😊
Partial<RenderContext<Props>>
// ...props = {}
const LabeledInput: TsxComponent<LabelInputProps> = ({props = {}}) => {
return (
<div>
<p>{props.name}</p>
<input type={props.type} />
</div>
)
}
learn from your idea @GavinRay97
type FunctionalProps<Props> = Partial<RenderContext<Props>> & Props;
export type FunctionalComponent<Props = {}> = (
props: FunctionalProps<Props>,
) => JSX.Element;
export function getProps<T>(context: FunctionalProps<T>): T {
return context.props!;
}
const Component: FunctionalComponent<{ a: number; b: string }> = (context) => {
const { a, b } = getProps(context);
return (
<div>
{a}
{b}
</div>
);
};
export const WrapComponent: FunctionalComponent = () => (
<Component a={1} b="" />
);