Skip to content

Commit 6141fe0

Browse files
authored
Update ADVANCED.md
1 parent 1ac2dec commit 6141fe0

File tree

1 file changed

+27
-103
lines changed

1 file changed

+27
-103
lines changed

ADVANCED.md

+27-103
Original file line numberDiff line numberDiff line change
@@ -161,113 +161,47 @@ export interface Props {
161161
[Something to add? File an issue](https://github.com/sw-yx/react-typescript-cheatsheet/issues/new).
162162

163163

164-
## Types for Conditional Rendering
165-
166-
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.
167-
168-
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)*
164+
## Typing a Component that Accepts Different Props
169165

166+
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".
170167

168+
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.
171169
```tsx
172-
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
173-
174-
interface LinkProps {}
170+
type ButtonProps = JSX.IntrinsicElements['button']
171+
type AnchorProps = JSX.IntrinsicElements['a']
175172

176-
type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement>
177-
type RouterLinkProps = Omit<NavLinkProps, 'href'>
178-
179-
const Link = <T extends {}>(
180-
props: LinkProps & T extends RouterLinkProps ? RouterLinkProps : AnchorProps
181-
) => {
182-
if ((props as RouterLinkProps).to) {
183-
return <NavLink {...props as RouterLinkProps} />
184-
} else {
185-
return <a {...props as AnchorProps} />
186-
}
173+
// optionally use a custom type guard
174+
function isPropsForAnchorElement(props: ButtonProps | AnchorProps): props is AnchorProps {
175+
return 'href' in props
187176
}
188177

189-
<Link<RouterLinkProps> to="/">My link</Link> // ok
190-
<Link<AnchorProps> href="/">My link</Link> // ok
191-
<Link<RouterLinkProps> to="/" href="/">My link</Link> // error
178+
function Clickable(props: ButtonProps): JSX.Element
179+
function Clickable(props: AnchorProps): JSX.Element
180+
function Clickable(props: ButtonProps | AnchorProps) {
181+
if (isPropsForAnchorElement(props)) {
182+
return <a {...props} />
183+
} else {
184+
return <button {...props } />
185+
}
186+
}
192187
```
193188

194-
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:
195-
189+
They don't even need to be completely different props, as long as they have at least one difference in properties:
196190
```tsx
197191
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
192+
type LinkProps = Omit<JSX.IntrinsicElements[ 'a' ], 'href'> & { to?: string }
198193

199-
type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement>
200-
type RouterLinkProps = Omit<NavLinkProps, 'href'>
201-
202-
interface Button {
203-
as: React.ComponentClass | 'a'
204-
}
205-
206-
const Button: React.FunctionComponent<Button> = (props) => {
207-
const {as: Component, children, ...rest} = props
208-
return (
209-
<Component className="button" {...rest}>{children}</Component>
210-
)
211-
}
212-
213-
const AnchorButton: React.FunctionComponent<AnchorProps> = (props) => (
214-
<Button as="a" {...props} />
215-
)
216-
217-
const LinkButton: React.FunctionComponent<RouterLinkProps> = (props) => (
218-
<Button as={NavLink} {...props} />
219-
)
220-
221-
<LinkButton to="/login">Login</LinkButton>
222-
<AnchorButton href="/login">Login</AnchorButton>
223-
<AnchorButton href="/login" to="/test">Login</AnchorButton> // Error: Property 'to' does not exist on type...
224-
```
225-
226-
## Props: One or the Other but not Both
227-
228-
Use the `in` keyword, function overloading, and union types to make components that take either one or another sets of props, but not both:
229-
230-
```tsx
231-
type Props1 = { foo: string }
232-
type Props2 = { bar: string }
233-
234-
function MyComponent(props: Props1): JSX.Element
235-
function MyComponent(props: Props2): JSX.Element
236-
function MyComponent(props: Props1 | Props2) {
237-
if ('foo' in props) {
238-
// props.bar // error
239-
return <div>{props.foo}</div>
194+
function RouterLink(props: LinkProps): JSX.Element
195+
function RouterLink(props: AnchorProps): JSX.Element
196+
function RouterLink(props: LinkProps | AnchorProps) {
197+
if ('to' in props) {
198+
return <a {...props} />
240199
} else {
241-
// props.foo // error
242-
return <div>{props.bar}</div>
200+
return <Link {...props } />
243201
}
244202
}
245-
const UsageComponent: React.FC = () => (
246-
<div>
247-
<MyComponent foo="foo" />
248-
<MyComponent bar="bar" />
249-
{/* <MyComponent foo="foo" bar="bar"/> // invalid */}
250-
</div>
251-
)
252203
```
253204

254-
255-
## Props: Must Pass Both
256-
257-
```tsx
258-
type OneOrAnother<T1, T2> =
259-
| (T1 & { [K in keyof T2]?: undefined })
260-
| (T2 & { [K in keyof T1]?: undefined })
261-
262-
type Props = OneOrAnother<{ a: string; b: string }, {}>
263-
264-
const a: Props = { a: 'a' } // error
265-
const b: Props = { b: 'b' } // error
266-
const ab: Props = { a: 'a', b: 'b' } // ok
267-
```
268-
269-
Thanks [diegohaz](https://twitter.com/kentcdodds/status/1085655423611367426)
270-
271205
## Omit attribute from a type
272206

273207
Sometimes when intersecting types, we want to define our own version of an attribute. For example, I want my component to have a `label`, but the type I am intersecting with also has a `label` attribute. Here's how to extract that out:
@@ -317,20 +251,10 @@ The advantage of extracting the prop types is that you won't need to export ever
317251

318252

319253
```ts
320-
// helper type for all known valid JSX element constructors (class and function based)
321-
type ElementConstructor<P> =
322-
| ((props: P) => React.ReactElement<any> | null)
323-
| (new (props: P) => React.Component<P, any, any>);
324-
325-
// gets the internal props of a component
326-
// used like Props<typeof MyComponent>
327-
// or Props<'button'> for intrinsic HTML attributes
328-
type Props<C> = C extends ElementConstructor<infer P>
329-
? P
330-
: C extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[C] : {};
254+
import { ComponentProps, JSXElementConstructor } from 'react'
331255

332256
// goes one step further and resolves with propTypes and defaultProps properties
333-
type ApparentProps<C> = C extends ElementConstructor<infer P> ? JSX.LibraryManagedAttributes<C, P> : Props<C>
257+
type ApparentComponentProps<C> = C extends JSXElementConstructor<infer P> ? JSX.LibraryManagedAttributes<C, P> : ComponentProps<C>
334258
```
335259
336260
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)