2

We are trying to migrate to react 18.

The children prop was removed from React.FunctionComponent (React.FC) so you have to declare it explicitly in your component properties.

But we want to keep the old React.FunctionComponent with children behavior. So, we are tryibg to override react types by creating a custom type definition file index.d.ts. This way we wont have to change 100s of components manually.

import * as React from '@types/react';

declare module 'react' {
  //type PropsWithChildren<P> = P & { children?: ReactNode | undefined };
  interface FunctionComponent<P = {}> {
    (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
  }
}

We have components like below.


interface IProps {
  age: number
}

const Demo: React.FunctionComponent<IProps> = ({
  children,
  age
}) => (
  <>
    {children}
    {age}
  </>
);

<Demo age={10}>Hi</Demo>

After overriding react types we are now getting following error in above Demo component.

Property 'children' does not exist on type 'IProps | PropsWithChildren<IProps>'

PropsWithChildren is defined in React 18 as below.

type PropsWithChildren<P> = P & { children?: ReactNode | undefined };

Below is the codesandbox link

https://codesandbox.io/s/quizzical-goodall-9d9s3j?file=/src/Demo.tsx

Ref :- React 18 TypeScript children FC

Any help?

ajm
  • 12,863
  • 58
  • 163
  • 234
  • Why not add `children: ReactNode` in `IProps`? – Kiran Mar 10 '23 at 12:50
  • We are using NX monorepos and we have 100s of apps and libs each having 100s of components and we are trying to avoid touching them at least for now. Using this way we can at least migrate to react 18 and have teams fix such issues on app by app basis without breaking anything. – ajm Mar 10 '23 at 12:51
  • 1
    Interface merging won't solve your problem. The local declaration will just act as an overload for the existing type. If leaving your existing code untouched is a hard requirement you can resort to patching the React type with something like [patch-package](https://www.npmjs.com/package/patch-package). – Andrew Gillis Mar 16 '23 at 16:30

2 Answers2

3

General answer – you need to follow new rules of React 18, and you should change 100s files

The normal solution, for this case, will be to manually update component props interfaces to add children only where's needed. But if it's overkill for you, then add it to every component:

  1. Use your IDE global "find & replace" tool find&replace
  2. Replace all
    React.FunctionComponent<
    with
    React.FunctionComponent<{children?: ReactNode | undefined } &

It'll convert all your functional components to be like:

interface IProps {
  age: number
}

const Demo: React.FunctionComponent<{children?: ReactNode | undefined } & IProps> = ({
  children,
  age
}) => (
  <>
    {children}
    {age}
  </>
);

<Demo age={10}>Hi</Demo>

@alissa: it's nicer to do React.FunctionComponent<PropsWithChildren<IProps>> then is something changes in the children type your code will be in sync

Replace all
React.FunctionComponent<(\w+)>
with
React.FunctionComponent<PropsWithChildren<$1>>

Alexandr Tovmach
  • 3,071
  • 1
  • 14
  • 31
  • 2
    it's nicer to do React.FunctionComponent> then is something changes in the children type your code will be in sync – Alissa Mar 21 '23 at 06:06
  • Is PropsWithChildren safe to use? I mean it's not going to get deprecated in later versions of react like react 19 or so? – ajm Mar 21 '23 at 06:24
  • I can't say anything here, I prefer not to use it, because of syntax – Alexandr Tovmach Mar 21 '23 at 06:28
  • @ajm React are allowed to make breaking changes, but removing this type seems unlikely to me :) – Alissa Mar 21 '23 at 11:54
1

It seems that it's impossible to override React.FunctionComponent in a mannar that will replace the props parameter type.

The merging mechanism of a function interface, if you declare the same interface multiple times, is to add overloads of the function - create a more inclusive type. see docs

for example, if you declare interface A twice like

interface A {
  (a: string): void
}

interface A {
  (a: number): void
}

and then use interface A

const foo: A = (a) => {}

the type of a will be number | string. and not just number.

Which makes sense, because someone might have already written some code assuming that A receives a parameter of type string.

Though I did manage to create a new type with the correct props

interface FunctionComponentWithChildren<P> extends Omit<React.FunctionComponent<P>, "parameters"> {
  (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
}

I think I would add a FunctionComponentWithChildren type and change my code to use it.

Alissa
  • 1,824
  • 1
  • 12
  • 15
  • Not sure what you trying to solve here? Like how it answer to a question? – Alexandr Tovmach Mar 21 '23 at 09:45
  • @AlexandrTovmach the question was how to override the FunctionComponent interface props parameter, my answer basically says that it seems that it's impossible. and suggests an alternative. have I misunderstood the question? – Alissa Mar 21 '23 at 11:23
  • I reordered my answer a bit, I hope it's clearer – Alissa Mar 21 '23 at 11:35