Skip to content

Commit 70961a4

Browse files
authored
Merge pull request #71 from sw-yx/advanced-components
advanced components revision
2 parents e474dc2 + 1c12b2b commit 70961a4

File tree

1 file changed

+55
-16
lines changed

1 file changed

+55
-16
lines changed

ADVANCED.md

+55-16
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,52 @@ export interface Props {
180180
[Something to add? File an issue](https://github.com/sw-yx/react-typescript-cheatsheet/issues/new).
181181
182182
183-
## Types for Conditional Rendering
183+
## Typing a Component that Accepts Different Props
184184
185-
Components can render different things based on props that are passed in, and this can be confusing to model in terms of argument and return types. See the Type checks, guards, and assertion strategies discussed above as a first resort.
185+
Components, and JSX in general, are analogous to functions. When a component can render differently based on their props, it's similar to how a function can be overloaded to have multiple call signatures. In the same way, you can overload a function component's call signature to list all of its different "versions".
186186
187-
You can also do fairly advanced logic within your types ([they are Turing complete!](https://github.com/Microsoft/TypeScript/issues/14833)). Read the [Advanced Types](https://www.typescriptlang.org/docs/handbook/advanced-types.html) section of the docs for ideas on how to use `Pick`, `ReadOnly`, `Partial`, and `Record`. Here is an example solution, see the further discussion for other solutions. *thanks to [@jpavon](https://github.com/sw-yx/react-typescript-cheatsheet/issues/12#issuecomment-394440577)*
187+
A very common use case for this is to render something as either a button or an anchor, based on if it receives a `href` attribute.
188+
```tsx
189+
type ButtonProps = JSX.IntrinsicElements['button']
190+
type AnchorProps = JSX.IntrinsicElements['a']
191+
192+
// optionally use a custom type guard
193+
function isPropsForAnchorElement(props: ButtonProps | AnchorProps): props is AnchorProps {
194+
return 'href' in props
195+
}
188196

197+
function Clickable(props: ButtonProps): JSX.Element
198+
function Clickable(props: AnchorProps): JSX.Element
199+
function Clickable(props: ButtonProps | AnchorProps) {
200+
if (isPropsForAnchorElement(props)) {
201+
return <a {...props} />
202+
} else {
203+
return <button {...props } />
204+
}
205+
}
206+
```
189207

208+
They don't even need to be completely different props, as long as they have at least one difference in properties:
209+
```tsx
210+
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
211+
type LinkProps = Omit<JSX.IntrinsicElements[ 'a' ], 'href'> & { to?: string }
212+
213+
function RouterLink(props: LinkProps): JSX.Element
214+
function RouterLink(props: AnchorProps): JSX.Element
215+
function RouterLink(props: LinkProps | AnchorProps) {
216+
if ('to' in props) {
217+
return <a {...props} />
218+
} else {
219+
return <Link {...props } />
220+
}
221+
}
222+
```
223+
224+
<details>
225+
<summary><b>Approach: Generic Components</b></summary>
226+
227+
Here is an example solution, see the further discussion for other solutions. *thanks to [@jpavon](https://github.com/sw-yx/react-typescript-cheatsheet/issues/12#issuecomment-394440577)*
228+
190229
```tsx
191230
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
192231
@@ -209,8 +248,15 @@ const Link = <T extends {}>(
209248
<Link<AnchorProps> href="/">My link</Link> // ok
210249
<Link<RouterLinkProps> to="/" href="/">My link</Link> // error
211250
```
251+
252+
</details>
253+
212254

213-
If you want to conditionaly render a component, sometimes is better to use [React's composition model](https://reactjs.org/docs/composition-vs-inheritance.html) to have simpler components and better to understand typings:
255+
256+
<details>
257+
<summary><b>Approach: Composition</b></summary>
258+
259+
If you want to conditionally render a component, sometimes is better to use [React's composition model](https://reactjs.org/docs/composition-vs-inheritance.html) to have simpler components and better to understand typings:
214260

215261
```tsx
216262
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
@@ -241,6 +287,9 @@ const LinkButton: React.FunctionComponent<RouterLinkProps> = (props) => (
241287
<AnchorButton href="/login">Login</AnchorButton>
242288
<AnchorButton href="/login" to="/test">Login</AnchorButton> // Error: Property 'to' does not exist on type...
243289
```
290+
</details>
291+
292+
244293

245294
## Props: One or the Other but not Both
246295

@@ -332,20 +381,10 @@ The advantage of extracting the prop types is that you won't need to export ever
332381
333382
334383
```ts
335-
// helper type for all known valid JSX element constructors (class and function based)
336-
type ElementConstructor<P> =
337-
| ((props: P) => React.ReactElement<any> | null)
338-
| (new (props: P) => React.Component<P, any, any>);
339-
340-
// gets the internal props of a component
341-
// used like Props<typeof MyComponent>
342-
// or Props<'button'> for intrinsic HTML attributes
343-
type Props<C> = C extends ElementConstructor<infer P>
344-
? P
345-
: C extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[C] : {};
384+
import { ComponentProps, JSXElementConstructor } from 'react'
346385

347386
// goes one step further and resolves with propTypes and defaultProps properties
348-
type ApparentProps<C> = C extends ElementConstructor<infer P> ? JSX.LibraryManagedAttributes<C, P> : Props<C>
387+
type ApparentComponentProps<C> = C extends JSXElementConstructor<infer P> ? JSX.LibraryManagedAttributes<C, P> : ComponentProps<C>
349388
```
350389
351390
You can also use them to strongly type custom event handlers if they're not written at the call sites themselves

0 commit comments

Comments
 (0)