Skip to content

Commit

Permalink
fix: Forward ref type inference
Browse files Browse the repository at this point in the history
  • Loading branch information
GianlucaGuarini committed Feb 7, 2024
1 parent 7a6a7e8 commit 91afa1c
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 124 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,9 @@ const HeadingInner = <T extends HeadingAllowedElements>(

// Forward refs with generics is tricky
// see also https://fettblog.eu/typescript-react-generic-forward-refs/
export const Heading = forwardRef<HeadingAllowedElements>(HeadingInner) as <T extends HeadingAllowedElements>(
export const Heading = forwardRef<HeadingAllowedElements>(HeadingInner) as unknown as <
T extends HeadingAllowedElements,
>(
// eslint-disable-next-line no-use-before-define
props: HeadingProps<T> & { ref?: PolymorphicForwardedRef<T> },
) => ReturnType<typeof HeadingInner>;
Expand Down
5 changes: 2 additions & 3 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ComponentPropsWithRef,
FC,
ForwardedRef,

Check failure on line 9 in index.d.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

'ForwardedRef' is defined but never used
HTMLAttributes,

Check failure on line 10 in index.d.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

'HTMLAttributes' is defined but never used
} from 'react';

// Utility type to merge two types
Expand Down Expand Up @@ -51,6 +52,4 @@ export type PolymorphicFunctionalProps<
> = T extends FC<infer U> ? PolymorphicProps<Merge<P, PropsWithoutAs<PropsWithoutRef<U>>>, T, S> : never;

// Type for the forwarded ref of a component
export type PolymorphicForwardedRef<C extends ElementType> = C extends keyof JSX.IntrinsicElements
? ForwardedRef<HTMLMapElement[C]>
: ComponentPropsWithRef<C>['ref'];
export type PolymorphicForwardedRef<C extends ElementType> = ComponentPropsWithRef<C>['ref'];
224 changes: 112 additions & 112 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
"homepage": "https://github.com/axa-ch/react-polymorphic-types#readme",
"devDependencies": {
"@axa-ch/easy-config": "^1.2.2",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint": "^8.56.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^9.1.0",
Expand All @@ -51,11 +51,11 @@
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"framer-motion": "^11.0.3",
"prettier": "^3.2.4",
"prettier": "^3.2.5",
"typescript": "^5.3.3"
},
"dependencies": {
"@types/react": "^18.2.48",
"@types/react": "^18.2.55",
"react": "^18.2.0"
}
}
21 changes: 21 additions & 0 deletions tests/button.ref.specs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { forwardRef, useRef } from 'react';
import { Button } from './components/button.ref';

export default () => {
const ref = useRef<HTMLButtonElement | null>(null);

return (
<Button
ref={ref}
as='button'
/>
);
};

export const ForwardedButton = forwardRef<HTMLButtonElement>(({ ...props }, ref) => (
<Button
{...props}
ref={ref}
as={'button'}
/>
));
53 changes: 53 additions & 0 deletions tests/components/button.ref.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ComponentPropsWithoutRef, createElement, forwardRef, FC, ExoticComponent } from 'react';
import {
PolymorphicExoticProps,
PolymorphicForwardedRef,
PolymorphicFunctionalProps,
PolymorphicProps,
} from '../../index';

// Default HTML element if the "as" prop is not provided
export const ButtonDefaultElement = 'button';
// List of allowed HTML Element that can be passed via "as" prop
export type ButtonAllowedElements = typeof ButtonDefaultElement | 'a' | 'div' | 'span';
// List of allowed React nodes that can be passed along with the "as" prop
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
export type ButtonAllowedComponentTypes = ButtonAllowedElements | ExoticComponent | FC<any>;

// Component-specific props
export type ButtonOwnProps<T extends ButtonAllowedElements> = ComponentPropsWithoutRef<T> & {
variant?: 'primary' | 'secondary' | 'tertiary';
disabled?: boolean;
};

// Extend own props with others inherited from the underlying element type
// Own props take precedence over the inherited ones
export type ButtonProps<T extends ButtonAllowedComponentTypes> = T extends ButtonAllowedElements
? PolymorphicProps<ButtonOwnProps<T>, T, ButtonAllowedElements>
: /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
T extends FC<any>
? PolymorphicFunctionalProps<ButtonOwnProps<ButtonAllowedElements>, T, ButtonAllowedElements>
: PolymorphicExoticProps<ButtonOwnProps<ButtonAllowedElements>, T, ButtonAllowedElements>;

const ButtonInner = <T extends ButtonAllowedComponentTypes>(
{ as = ButtonDefaultElement, children, ...rest }: ButtonProps<T>,
ref: PolymorphicForwardedRef<T>,
) =>
createElement(
as,
{
...(rest.disabled && as !== 'button' ? { 'data-disabled': 'disabled' } : null),
...rest,
ref,
},
children,
);

// Forward refs with generics is tricky
// see also https://fettblog.eu/typescript-react-generic-forward-refs/
export const Button = forwardRef<ButtonAllowedComponentTypes>(ButtonInner) as <
T extends ButtonAllowedComponentTypes = typeof ButtonDefaultElement,
>(
// eslint-disable-next-line no-use-before-define
props: ButtonProps<T> & { ref?: PolymorphicForwardedRef<T> },
) => ReturnType<typeof ButtonInner>;
2 changes: 1 addition & 1 deletion tests/components/complex.ref.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const ComplexInner = <T extends ComplexAllowedElements>(

// Forward refs with generics is tricky
// see also https://fettblog.eu/typescript-react-generic-forward-refs/
export const Complex = forwardRef(ComplexInner) as <T extends ComplexAllowedElements>(
export const Complex = forwardRef(ComplexInner) as unknown as <T extends ComplexAllowedElements>(
// eslint-disable-next-line no-use-before-define
props: ComplexProps<T> & { ref?: PolymorphicForwardedRef<T> },
) => ReturnType<typeof ComplexInner>;
2 changes: 1 addition & 1 deletion tests/components/ref.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const HeadingInner = <T extends HeadingAllowedElements>(

// Forward refs with generics is tricky
// see also https://fettblog.eu/typescript-react-generic-forward-refs/
export const Heading = forwardRef(HeadingInner) as <T extends HeadingAllowedElements>(
export const Heading = forwardRef(HeadingInner) as unknown as <T extends HeadingAllowedElements>(
// eslint-disable-next-line no-use-before-define
props: HeadingProps<T> & { ref?: PolymorphicForwardedRef<T> },
) => ReturnType<typeof HeadingInner>;
2 changes: 1 addition & 1 deletion tests/ref.specs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useRef } from 'react';
import { Heading } from './components/ref';

export default () => {
const ref = useRef<HTMLElement | null>(null);
const ref = useRef<HTMLHeadingElement | null>(null);

return (
<Heading
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"extends": "@axa-ch/easy-config/ts-config/base",
"extends": "@axa-ch/easy-config/ts-config/base"
}

0 comments on commit 91afa1c

Please sign in to comment.