jsx-vue2 icon indicating copy to clipboard operation
jsx-vue2 copied to clipboard

Functional Components with TypeScript

Open webistomin opened this issue 4 years ago • 5 comments

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.

webistomin avatar May 07 '20 05:05 webistomin

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.

Trendymen avatar May 20 '20 03:05 Trendymen

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>
  )
}

tsx-types

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.

tsx-types-2

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.

GavinRay97 avatar Jul 09 '20 02:07 GavinRay97

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 😊

webistomin avatar Jul 19 '20 11:07 webistomin

Partial<RenderContext<Props>>
// ...props = {}
const LabeledInput: TsxComponent<LabelInputProps> = ({props = {}}) => {
  return (
    <div>
      <p>{props.name}</p>
      <input type={props.type} />
    </div>
  )
}

djkloop avatar Jun 02 '21 10:06 djkloop

learn from your idea @GavinRay97 image

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="" />
);

holy-func avatar Oct 20 '23 03:10 holy-func