Skip to content

Commit 02b7379

Browse files
jfuchscolebemis
andauthored
Introduces a Merge<> utility and a type testing pattern (#1505)
* Introduces a Merge<> utility and a type testing pattern * Fix the glob to include tests * Update src/TextInput.tsx Co-authored-by: Cole Bemis <[email protected]> Co-authored-by: Cole Bemis <[email protected]>
1 parent 54918f6 commit 02b7379

File tree

10 files changed

+137
-25
lines changed

10 files changed

+137
-25
lines changed

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ module.exports = {
99
'<rootDir>/src/utils/test-deprecations.tsx',
1010
'<rootDir>/src/utils/test-helpers.tsx'
1111
],
12-
testRegex: '/(src|codemods)/__tests__/.*\\.test.[jt]sx?$'
12+
testMatch: ['<rootDir>/(src|codemods)/__tests__/**/*.test.[jt]s?(x)', '!**/*.types.test.[jt]s?(x)']
1313
}

src/ActionList/List.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import styled from 'styled-components'
77
import {get} from '../constants'
88
import {SystemCssProperties} from '@styled-system/css'
99
import {hasActiveDescendantAttribute} from '../behaviors/focusZone'
10+
import {Merge} from '../utils/types/Merge'
1011

1112
type RenderItemFn = (props: ItemProps) => React.ReactElement
1213

1314
export type ItemInput =
14-
| (ItemProps & Omit<React.ComponentPropsWithoutRef<'div'>, keyof ItemProps>)
15+
| Merge<React.ComponentPropsWithoutRef<'div'>, ItemProps>
1516
| ((Partial<ItemProps> & {renderItem: RenderItemFn}) & {key?: Key})
1617

1718
/**

src/TextInput.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import styled, {css} from 'styled-components'
44
import {maxWidth, MaxWidthProps, minWidth, MinWidthProps, variant, width, WidthProps} from 'styled-system'
55
import {get} from './constants'
66
import sx, {SxProp} from './sx'
7-
import {ComponentProps} from './utils/types'
7+
import {ComponentProps, Merge} from './utils/types'
88

99
const sizeVariants = variant({
1010
variants: {
@@ -127,9 +127,8 @@ type NonPassthroughProps = {
127127
'block' | 'contrast' | 'disabled' | 'sx' | 'theme' | 'width' | 'maxWidth' | 'minWidth' | 'variant'
128128
>
129129

130-
type TextInputInternalProps = NonPassthroughProps &
131-
// Note: using ComponentProps instead of ComponentPropsWithoutRef here would cause a type issue where `css` is a required prop.
132-
Omit<React.ComponentPropsWithoutRef<typeof Input>, keyof NonPassthroughProps>
130+
// Note: using ComponentProps instead of ComponentPropsWithoutRef here would cause a type issue where `css` is a required prop.
131+
type TextInputInternalProps = Merge<React.ComponentPropsWithoutRef<typeof Input>, NonPassthroughProps>
133132

134133
// using forwardRef is important so that other components (ex. SelectMenu) can autofocus the input
135134
const TextInput = React.forwardRef<HTMLInputElement, TextInputInternalProps>(
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from 'react'
2+
import {ActionList} from '..'
3+
4+
export function emptyList() {
5+
return <ActionList items={[]} />
6+
}
7+
8+
export function listWithSingleItem() {
9+
return <ActionList items={[{text: 'One'}]} />
10+
}
11+
12+
export function canUseDivDOMProps() {
13+
return (
14+
<ActionList
15+
items={[
16+
{
17+
text: 'One',
18+
onMouseDown: () => undefined
19+
}
20+
]}
21+
/>
22+
)
23+
}
24+
25+
export function cannotUseAnchorDOMProps() {
26+
return (
27+
<ActionList
28+
items={[
29+
{
30+
text: 'One',
31+
// @ts-expect-error href is not a div DOM prop
32+
href: '#'
33+
}
34+
]}
35+
/>
36+
)
37+
}
38+
39+
export function cannotUseAsWithoutRenderProp() {
40+
return (
41+
<ActionList
42+
items={[
43+
{
44+
text: 'One',
45+
// @ts-expect-error as is only available via manual rendering of items
46+
as: 'a'
47+
}
48+
]}
49+
/>
50+
)
51+
}

src/__tests__/Merge.types.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {Merge} from '../utils/types/Merge'
2+
3+
type AString = {
4+
a: string
5+
}
6+
7+
type BString = {
8+
b: string
9+
}
10+
11+
type CString = {
12+
c: string
13+
}
14+
15+
type CNumber = {
16+
c: number
17+
}
18+
19+
type DOptionalString = {
20+
d?: string
21+
}
22+
23+
export function canMergeTwoTypes(x: Merge<AString, BString>): {a: string; b: string} {
24+
return x
25+
}
26+
27+
export function overridesAsExpected(x: Merge<CString, CNumber>): {c: number} {
28+
return x
29+
}
30+
31+
export function optionalityIsPreservedInFirstParameter(x: Merge<DOptionalString, AString>): {d: number} {
32+
// @ts-expect-error: d is optional
33+
return x
34+
}
35+
36+
export function optionalityIsPreservedInSecondParameter(x: Merge<DOptionalString, AString>): {d: number} {
37+
// @ts-expect-error: d is optional
38+
return x
39+
}

src/utils/types.ts renamed to src/utils/types/AriaRole.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,3 @@
1-
/**
2-
* Extract a component's props
3-
*
4-
* Source: https://react-typescript-cheatsheet.netlify.app/docs/advanced/patterns_by_usecase#wrappingmirroring-a-component
5-
*
6-
* @example ComponentProps<typeof MyComponent>
7-
*/
8-
export type ComponentProps<T> = T extends React.ComponentType<infer Props>
9-
? // eslint-disable-next-line @typescript-eslint/ban-types
10-
Props extends object
11-
? Props
12-
: never
13-
: never
14-
15-
/**
16-
* Contruct a type describing the items in `T`, if `T` is an array.
17-
*/
18-
export type Flatten<T extends unknown> = T extends (infer U)[] ? U : never
19-
201
// ref: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques
212
export type AriaRole =
223
| 'alert'

src/utils/types/ComponentProps.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Extract a component's props
3+
*
4+
* Source: https://react-typescript-cheatsheet.netlify.app/docs/advanced/patterns_by_usecase#wrappingmirroring-a-component
5+
*
6+
* @example ComponentProps<typeof MyComponent>
7+
*/
8+
export type ComponentProps<T> = T extends React.ComponentType<infer Props>
9+
? // eslint-disable-next-line @typescript-eslint/ban-types
10+
Props extends object
11+
? Props
12+
: never
13+
: never

src/utils/types/Flatten.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* Contruct a type describing the items in `T`, if `T` is an array.
3+
*/
4+
export type Flatten<T extends unknown> = T extends (infer U)[] ? U : never

src/utils/types/Merge.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Given two object types A and B, return a type with all the properties of A that aren't also
3+
* properties of B, and all the properties of B.
4+
*
5+
* Useful when we have a component that spreads a "rest" of its props on a subcomponent:
6+
*
7+
* ```ts
8+
* interface OwnProps {
9+
* foo: string
10+
* }
11+
*
12+
* type MyComponentProps = Merge<SubcomponentProps, OwnProps>
13+
* const MyComponent = ({foo, ...rest}: MyComponentProps) => {
14+
* // ...
15+
* return <SubComponent {...rest} />
16+
* }
17+
* ```
18+
*/
19+
// eslint-disable-next-line @typescript-eslint/ban-types
20+
export type Merge<A = {}, B = {}> = Omit<A, keyof B> & B

src/utils/types/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './AriaRole'
2+
export * from './ComponentProps'
3+
export * from './Flatten'
4+
export * from './Merge'

0 commit comments

Comments
 (0)