Skip to content

Commit 490ca0b

Browse files
committed
Possible forwardRef solution
1 parent 757f4c5 commit 490ca0b

File tree

5 files changed

+114
-42
lines changed

5 files changed

+114
-42
lines changed

src/08-advanced-patterns/72-as-prop-with-default.problem.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const Link = <T extends ElementType>(
1010
return <Comp {...rest}></Comp>;
1111
};
1212

13-
// Should be a 'a' tag by default!
13+
// 1. Should be a 'a' tag by default!
1414
<Link href="/"></Link>;
1515

1616
const Custom = (props: { thisIsRequired: boolean }) => {
@@ -22,6 +22,7 @@ const Custom = (props: { thisIsRequired: boolean }) => {
2222
// @ts-expect-error Property 'thisIsRequired' is missing
2323
<Link as={Custom} />;
2424

25+
// 2. Should still give you autocomplete options!
2526
<Link
2627
as="button"
2728
onClick={(e) => {

src/08-advanced-patterns/72-as-prop-with-default.solution.tsx renamed to src/08-advanced-patterns/72-as-prop-with-default.solution.1.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { ComponentPropsWithoutRef, ElementType } from "react";
22
import { Equal, Expect } from "../helpers/type-utils";
33

4+
/**
5+
* This NEARLY works, but removes autocomplete for the 'as' prop.
6+
*/
47
export const Link = <T extends ElementType = "a">(
58
props: {
69
as?: T;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { ComponentPropsWithoutRef, ElementType } from "react";
2+
import { Equal, Expect } from "../helpers/type-utils";
3+
4+
/**
5+
* Function overloads are another solution which work. We're getting
6+
* closer to extreme verbosity, but it works!
7+
*
8+
* Though - the error at the bottom of the file becomes much
9+
* harder to read.
10+
*/
11+
12+
function Link<T extends ElementType>(
13+
props: {
14+
as: T;
15+
} & ComponentPropsWithoutRef<T>,
16+
): React.ReactNode;
17+
function Link(props: ComponentPropsWithoutRef<"a">): React.ReactNode;
18+
function Link<T extends ElementType>(
19+
props: {
20+
as?: T;
21+
} & ComponentPropsWithoutRef<T>,
22+
) {
23+
const { as: Comp = "a", ...rest } = props;
24+
return <Comp {...rest}></Comp>;
25+
}
26+
27+
<Link href="/"></Link>;
28+
29+
const Custom = (props: { thisIsRequired: boolean }) => {
30+
return null;
31+
};
32+
33+
<Link as={Custom} thisIsRequired />;
34+
35+
// @ts-expect-error Property 'thisIsRequired' is missing
36+
<Link as={Custom} />;
37+
38+
<Link
39+
as="button"
40+
onClick={(e) => {
41+
type test = Expect<Equal<typeof e, React.MouseEvent<HTMLButtonElement>>>;
42+
}}
43+
></Link>;
44+
45+
// @ts-expect-error: Property 'href' does not exist
46+
<Link as="div" href="awdawd"></Link>;
Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1-
import { ComponentPropsWithoutRef, ElementType, forwardRef } from "react";
1+
import {
2+
ComponentPropsWithoutRef,
3+
ElementType,
4+
JSXElementConstructor,
5+
forwardRef,
6+
} from "react";
27
import { Equal, Expect } from "../helpers/type-utils";
38

4-
export const Link = forwardRef(
5-
<T extends ElementType = "a">(
6-
props: {
7-
as?: T;
8-
} & ComponentPropsWithoutRef<T>,
9-
) => {
10-
const { as: Comp = "a", ...rest } = props;
11-
return <Comp {...rest}></Comp>;
12-
},
13-
);
9+
function UnwrappedLink<
10+
T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>,
11+
>(
12+
props: {
13+
as?: T;
14+
} & ComponentPropsWithoutRef<T>,
15+
) {
16+
const { as: Comp = "a", ...rest } = props;
17+
return <Comp {...rest}></Comp>;
18+
}
19+
20+
const Link = forwardRef(UnwrappedLink);
1421

1522
<Link href="/"></Link>;
1623

@@ -30,8 +37,5 @@ const Custom = (props: { thisIsRequired: boolean }) => {
3037
}}
3138
></Link>;
3239

33-
<Link
34-
as="div"
35-
// @ts-expect-error: Property 'href' does not exist
36-
href="awdawd"
37-
></Link>;
40+
// @ts-expect-error: Property 'href' does not exist
41+
<Link as="div" href="awdawd"></Link>;
Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import {
22
ComponentProps,
3+
ComponentPropsWithRef,
34
ComponentPropsWithoutRef,
45
ElementType,
6+
JSXElementConstructor,
57
forwardRef,
68
useRef,
79
} from "react";
@@ -13,23 +15,28 @@ type FixedForwardRef = <T, P = {}>(
1315

1416
const fixedForwardRef = forwardRef as FixedForwardRef;
1517

16-
export const Link = fixedForwardRef(
17-
<T extends ElementType = "a">(
18-
props: {
19-
as?: T;
20-
} & ComponentProps<T>,
21-
) => {
22-
const { as = "a", ...rest } = props;
23-
const Comp = as as any;
24-
return <Comp {...rest}></Comp>;
25-
},
26-
);
27-
28-
const Test = () => {
29-
const ref = useRef<HTMLAnchorElement>(null);
30-
31-
return <Link as="a" href="/" ref={ref}></Link>;
32-
};
18+
type Constraint = keyof JSX.IntrinsicElements | JSXElementConstructor<any>;
19+
20+
type Props<
21+
T extends Constraint,
22+
P = ComponentProps<Constraint extends T ? "a" : T>,
23+
> = P;
24+
25+
type Example = Props<"button">;
26+
27+
function UnwrappedLink<T extends Constraint>(props: { as?: T } & Props<T>) {
28+
const { as: Comp = "a", ...rest } = props;
29+
return <Comp {...rest}></Comp>;
30+
}
31+
32+
const Link = fixedForwardRef(UnwrappedLink);
33+
34+
<Link
35+
href="/"
36+
onClick={(e) => {
37+
type test = Expect<Equal<typeof e, React.MouseEvent<HTMLAnchorElement>>>;
38+
}}
39+
></Link>;
3340

3441
const Custom = (props: { thisIsRequired: boolean }) => {
3542
return null;
@@ -40,18 +47,29 @@ const Custom = (props: { thisIsRequired: boolean }) => {
4047
// @ts-expect-error Property 'thisIsRequired' is missing
4148
<Link as={Custom} />;
4249

43-
const ref = useRef<HTMLButtonElement>(null);
44-
4550
<Link
4651
as="button"
4752
onClick={(e) => {
4853
type test = Expect<Equal<typeof e, React.MouseEvent<HTMLButtonElement>>>;
4954
}}
50-
ref={ref}
5155
></Link>;
5256

53-
<Link
54-
as="div"
55-
// @ts-expect-error: Property 'href' does not exist
56-
href="awdawd"
57-
></Link>;
57+
const ref = useRef<HTMLAnchorElement>(null);
58+
const wrongRef = useRef<HTMLDivElement>(null);
59+
60+
Link({
61+
ref,
62+
});
63+
64+
Link({
65+
as: Custom,
66+
thisIsRequired: true,
67+
});
68+
69+
Link({
70+
// @ts-expect-error
71+
ref: wrongRef,
72+
});
73+
74+
// @ts-expect-error: Property 'href' does not exist
75+
<Link as="div" href="awdawd"></Link>;

0 commit comments

Comments
 (0)