Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Functional Components with TypeScript #137

Open
webistomin opened this issue May 7, 2020 · 5 comments
Open

Functional Components with TypeScript #137

webistomin opened this issue May 7, 2020 · 5 comments

Comments

@webistomin
Copy link

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.

@Trendymen
Copy link

Trendymen commented May 20, 2020

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.

@GavinRay97
Copy link

I have a working solution for this, but it's neither pretty nor technically correct 😢

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.

@webistomin
Copy link
Author

webistomin commented Jul 19, 2020

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 😊

@djkloop
Copy link

djkloop commented Jun 2, 2021

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

@holy-func
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants