|
47 | 47 | - [`as` props (passing a component to be rendered)](#as-props-passing-a-component-to-be-rendered)
|
48 | 48 | - [Types for Conditional Rendering](#types-for-conditional-rendering)
|
49 | 49 | - [Props: One or the Other but not Both](#props-one-or-the-other-but-not-both)
|
50 |
| - - [Props: Must Pass Both](#props-one-or-the-other-but-not-both) |
| 50 | + - [Props: Can Optionally Pass One Only If the Other Is Passed](#props-can-optionally-pass-one-only-if-the-other-is-passed) |
51 | 51 | - [Omit attribute from a type](#omit-attribute-from-a-type)
|
52 | 52 | - [Type Zoo](#type-zoo)
|
53 | 53 | - [Extracting Prop Types of a Component](#extracting-prop-types-of-a-component)
|
@@ -451,6 +451,81 @@ const ab: Props = { a: 'a', b: 'b' }; // ok
|
451 | 451 |
|
452 | 452 | Thanks [diegohaz](https://twitter.com/kentcdodds/status/1085655423611367426)
|
453 | 453 |
|
| 454 | +## Props: Can Optionally Pass One Only If the Other Is Passed |
| 455 | + |
| 456 | +Say you want a Text component that gets truncated if `truncate` prop is passed but expands to show the full text when `expanded` prop is passed (e.g. when the user clicks the text). |
| 457 | + |
| 458 | +You want to allow `expanded` to be passed only if `truncate` is also passed, because there is no use for `expanded` if the text is not truncated. |
| 459 | + |
| 460 | +You can do this by function overloads: |
| 461 | + |
| 462 | +```tsx |
| 463 | +import React from "react"; |
| 464 | + |
| 465 | +type CommonProps = { |
| 466 | + children: React.ReactNode; |
| 467 | + as: "p" | "span" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; |
| 468 | +}; |
| 469 | + |
| 470 | +type NoTruncateProps = CommonProps & { |
| 471 | + truncate?: false; |
| 472 | +}; |
| 473 | + |
| 474 | +type TruncateProps = CommonProps & { |
| 475 | + truncate: true; |
| 476 | + expanded?: boolean; |
| 477 | +}; |
| 478 | + |
| 479 | +// Type guard |
| 480 | +const isTruncateProps = ( |
| 481 | + props: NoTruncateProps | TruncateProps |
| 482 | +): props is TruncateProps => !!props.truncate; |
| 483 | + |
| 484 | +// Function overloads to accept both prop types NoTruncateProps & TruncateProps |
| 485 | +function Text(props: NoTruncateProps): JSX.Element; |
| 486 | +function Text(props: TruncateProps): JSX.Element; |
| 487 | +function Text(props: NoTruncateProps | TruncateProps) { |
| 488 | + |
| 489 | + if (isTruncateProps(props)) { |
| 490 | + const { children, as: Tag, truncate, expanded, ...otherProps } = props; |
| 491 | + |
| 492 | + const classNames = truncate ? ".truncate" : ""; |
| 493 | + |
| 494 | + return ( |
| 495 | + <Tag |
| 496 | + className={classNames} |
| 497 | + aria-expanded={!!expanded} |
| 498 | + {...otherProps} |
| 499 | + > |
| 500 | + {children} |
| 501 | + </Tag> |
| 502 | + ); |
| 503 | + } |
| 504 | + |
| 505 | + const { children, as: Tag, ...otherProps } = props; |
| 506 | + |
| 507 | + return <Tag {...otherProps}>{children}</Tag>; |
| 508 | +} |
| 509 | + |
| 510 | +Text.defaultProps = { |
| 511 | + as: 'span' |
| 512 | +} |
| 513 | +``` |
| 514 | + |
| 515 | +Using the Text component: |
| 516 | +```tsx |
| 517 | +const App: React.FC = () => ( |
| 518 | + <> |
| 519 | + <Text>not truncated</Text> {/* works */} |
| 520 | + <Text truncate>truncated</Text> {/* works */} |
| 521 | + <Text truncate expanded>truncate-able but expanded</Text> {/* works */} |
| 522 | + |
| 523 | + {/* TS error: Property 'truncate' is missing in type '{ children: string; expanded: true; }' but required in type 'Pick<TruncateProps, "expanded" | "children" | "truncate">'} */} |
| 524 | + <Text expanded>truncate-able but expanded</Text> |
| 525 | + </> |
| 526 | +); |
| 527 | +``` |
| 528 | + |
454 | 529 | ## Omit attribute from a type
|
455 | 530 |
|
456 | 531 | 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:
|
|
0 commit comments