Skip to content

Commit

Permalink
fix(react): add OmitKeyof safely and test cases for it (#816)
Browse files Browse the repository at this point in the history
# Overview

<!--
    A clear and concise description of what this pr is about. 
 -->

related with TanStack/query#7139

This change update `OmitKeyof` to validate Omitting keys should be keyof
Omitted Object Type

As shown below, the inconvenience of having to manually remove
unnecessary fields can be identified early on at the type level with
`OmitKeyof`.

### Motivatings
- https://github.com/TanStack/query/pull/6907/files
- TanStack/query#7139 (comment)

### Expectation
I made test-d to type-test `OmitKeyof`
- For complex types using `Omit`, I want to reduce the difficulty of
library maintenance for it.
    - `OmitKeyof` provide autocomplete by extending keyof TObject
![Mar-22-2024
18-12-27](https://github.com/TanStack/query/assets/61593290/3a3b1743-99cc-41bd-9f51-0a24f8973f87)
- `OmitKeyof` check strictly second type parameter of it by extending
keyof TObject
![Mar-22-2024
18-13-45](https://github.com/TanStack/query/assets/61593290/4204a036-af4f-4d19-b9fe-3aac39069f90)
- `OmitKeyof` prevent library contributors make misspell like queryTey
or qeuryKey
![Mar-22-2024
18-14-35](https://github.com/TanStack/query/assets/61593290/6c7e3ab0-a7bf-420f-a353-292e94297ec2)
- Library users can be supplied with stable types consistently

## PR Checklist

- [x] I did below actions if need

1. I read the [Contributing
Guide](https://github.com/suspensive/react/blob/main/CONTRIBUTING.md)
2. I added documents and tests.
  • Loading branch information
manudeli committed Mar 27, 2024
1 parent 0b8dfdb commit 33fe78c
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 31 deletions.
5 changes: 5 additions & 0 deletions .changeset/cyan-baboons-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@suspensive/react": patch
---

fix(react): add OmitKeyof safely and test cases for it
8 changes: 4 additions & 4 deletions packages/react/src/AsyncBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { type ComponentRef, forwardRef } from 'react'
import { ErrorBoundary, type ErrorBoundaryProps } from './ErrorBoundary'
import { Suspense, type SuspenseProps } from './Suspense'
import type { OmitKeyOf } from './utility-types'
import type { OmitKeyof } from './utility-types'

/**
* @deprecated Use SuspenseProps and ErrorBoundaryProps instead
*/
export interface AsyncBoundaryProps
extends OmitKeyOf<SuspenseProps, 'fallback' | 'devMode'>,
OmitKeyOf<ErrorBoundaryProps, 'fallback' | 'devMode'> {
extends OmitKeyof<SuspenseProps, 'fallback' | 'devMode'>,
OmitKeyof<ErrorBoundaryProps, 'fallback' | 'devMode'> {
pendingFallback?: SuspenseProps['fallback']
rejectedFallback: ErrorBoundaryProps['fallback']
}
Expand Down Expand Up @@ -38,7 +38,7 @@ export const AsyncBoundary = Object.assign(
* @deprecated Use `<Suspense clientOnly />` and `<ErrorBoundary/>` instead
*/
CSROnly: (() => {
const CSROnly = forwardRef<ComponentRef<typeof ErrorBoundary>, OmitKeyOf<AsyncBoundaryProps, 'clientOnly'>>(
const CSROnly = forwardRef<ComponentRef<typeof ErrorBoundary>, OmitKeyof<AsyncBoundaryProps, 'clientOnly'>>(
({ pendingFallback, rejectedFallback, children, ...errorBoundaryProps }, resetRef) => (
<ErrorBoundary {...errorBoundaryProps} ref={resetRef} fallback={rejectedFallback}>
<Suspense clientOnly fallback={pendingFallback}>
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/Suspense.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Suspense as ReactSuspense, type SuspenseProps as ReactSuspenseProps, useContext } from 'react'
import { SuspenseDefaultPropsContext, syncDevMode } from './contexts'
import { useIsClient } from './hooks'
import type { OmitKeyOf, PropsWithDevMode } from './utility-types'
import type { OmitKeyof, PropsWithDevMode } from './utility-types'

const SuspenseClientOnly = (props: ReactSuspenseProps) =>
useIsClient() ? <ReactSuspense {...props} /> : <>{props.fallback}</>
Expand Down Expand Up @@ -40,7 +40,7 @@ export const Suspense = Object.assign(
* @deprecated Use `<Suspense clientOnly/>` instead
*/
CSROnly: (() => {
const Suspense = ({ devMode, children, fallback }: OmitKeyOf<SuspenseProps, 'clientOnly'>) => {
const Suspense = ({ devMode, children, fallback }: OmitKeyof<SuspenseProps, 'clientOnly'>) => {
const defaultProps = useContext(SuspenseDefaultPropsContext)
return (
<SuspenseClientOnly fallback={typeof fallback === 'undefined' ? defaultProps.fallback : fallback}>
Expand Down
6 changes: 3 additions & 3 deletions packages/react/src/contexts/DefaultOptionsContexts.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { createContext } from 'react'
import type { DelayProps, SuspenseProps } from '..'
import type { OmitKeyOf } from '../utility-types'
import type { OmitKeyof } from '../utility-types'

export const DelayDefaultPropsContext = createContext<OmitKeyOf<DelayProps, 'children'>>({
export const DelayDefaultPropsContext = createContext<OmitKeyof<DelayProps, 'children'>>({
ms: undefined,
fallback: undefined,
})
if (process.env.NODE_ENV === 'development') {
DelayDefaultPropsContext.displayName = 'DelayDefaultPropsContext'
}

export const SuspenseDefaultPropsContext = createContext<OmitKeyOf<SuspenseProps, 'children' | 'devMode'>>({
export const SuspenseDefaultPropsContext = createContext<OmitKeyof<SuspenseProps, 'children' | 'devMode'>>({
fallback: undefined,
clientOnly: undefined,
})
Expand Down
1 change: 0 additions & 1 deletion packages/react/src/utility-types/OmitKeyOf.ts

This file was deleted.

176 changes: 176 additions & 0 deletions packages/react/src/utility-types/OmitKeyof.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { describe, expectTypeOf, it } from 'vitest'
import type { OmitKeyof } from './OmitKeyof'

describe('OmitKeyof', () => {
it("'s string key type check", () => {
type A = {
x: string
y: number
}

type ExpectedType = {
x: string
}

// Bad point
// 1. original Omit can use 'z' as type parameter with no type error
// 2. original Omit have no auto complete for 2nd type parameter
expectTypeOf<Omit<A, 'z' | 'y'>>().toEqualTypeOf<ExpectedType>()

// Solution

// 1. strictly
expectTypeOf<
OmitKeyof<
A,
// OmitKeyof can't use 'z' as type parameter with type error because A don't have key 'z'
// @ts-expect-error Type does not satisfy the constraint keyof A
'z' | 'y'
>
>().toEqualTypeOf<ExpectedType>
expectTypeOf<
OmitKeyof<
A,
// OmitKeyof can't use 'z' as type parameter with type error because A don't have key 'z'
// @ts-expect-error Type does not satisfy the constraint keyof A
'z' | 'y',
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-arguments
'strictly'
>
>().toEqualTypeOf<ExpectedType>

// 2. safely
expectTypeOf<
OmitKeyof<
A,
// OmitKeyof can't use 'z' as type parameter type error with strictly parameter or default parameter
// @ts-expect-error Type does not satisfy the constraint keyof A
'z' | 'y'
>
>().toEqualTypeOf<ExpectedType>
expectTypeOf<
OmitKeyof<
A,
// With 'safely', OmitKeyof can use 'z' as type parameter like original Omit but This support autocomplete too yet for DX.
'z' | 'y',
'safely'
>
>().toEqualTypeOf<ExpectedType>
})

it("'s number key type check", () => {
type A = {
[1]: string
[2]: number
}

type ExpectedType = {
[1]: string
}

// Bad point
// 1. original Omit can use 3 as type parameter with no type error
// 2. original Omit have no auto complete for 2nd type parameter
expectTypeOf<Omit<A, 3 | 2>>().toEqualTypeOf<ExpectedType>()

// Solution

// 1. strictly
expectTypeOf<
OmitKeyof<
A,
// OmitKeyof can't use 3 as type parameter with type error because A don't have key 3
// @ts-expect-error Type does not satisfy the constraint keyof A
3 | 2
>
>().toEqualTypeOf<ExpectedType>
expectTypeOf<
OmitKeyof<
A,
// OmitKeyof can't use 3 as type parameter with type error because A don't have key 3
// @ts-expect-error Type does not satisfy the constraint keyof A
3 | 2,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-arguments
'strictly'
>
>().toEqualTypeOf<ExpectedType>

// 2. safely
expectTypeOf<
OmitKeyof<
A,
// OmitKeyof can't use 3 as type parameter type error with strictly parameter or default parameter
// @ts-expect-error Type does not satisfy the constraint keyof A
3 | 2
>
>().toEqualTypeOf<ExpectedType>
expectTypeOf<
OmitKeyof<
A,
// With 'safely', OmitKeyof can use 3 as type parameter like original Omit but This support autocomplete too yet for DX.
3 | 2,
'safely'
>
>().toEqualTypeOf<ExpectedType>
})

it("'s symbol key type check", () => {
const symbol1 = Symbol()
const symbol2 = Symbol()
const symbol3 = Symbol()

type A = {
[symbol1]: string
[symbol2]: number
}

type ExpectedType = {
[symbol1]: string
}

// Bad point
// 1. original Omit can use symbol3 as type parameter with no type error
// 2. original Omit have no auto complete for 2nd type parameter
expectTypeOf<Omit<A, typeof symbol3 | typeof symbol2>>().toEqualTypeOf<ExpectedType>()

// Solution

// 1. strictly
expectTypeOf<
OmitKeyof<
A,
// OmitKeyof can't use symbol3 as type parameter with type error because A don't have key symbol3
// @ts-expect-error Type does not satisfy the constraint keyof A
typeof symbol3 | typeof symbol2
>
>().toEqualTypeOf<ExpectedType>
expectTypeOf<
OmitKeyof<
A,
// OmitKeyof can't use symbol3 as type parameter with type error because A don't have key symbol3
// @ts-expect-error Type does not satisfy the constraint keyof A
typeof symbol3 | typeof symbol2,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-arguments
'strictly'
>
>().toEqualTypeOf<ExpectedType>

// 2. safely
expectTypeOf<
OmitKeyof<
A,
// OmitKeyof can't use symbol3 as type parameter type error with strictly parameter or default parameter
// @ts-expect-error Type does not satisfy the constraint keyof A
typeof symbol3 | typeof symbol2
>
>().toEqualTypeOf<ExpectedType>
expectTypeOf<
OmitKeyof<
A,
// With 'safely', OmitKeyof can use symbol3 as type parameter like original Omit but This support autocomplete too yet for DX.
typeof symbol3 | typeof symbol2,
'safely'
>
>().toEqualTypeOf<ExpectedType>
})
})
11 changes: 11 additions & 0 deletions packages/react/src/utility-types/OmitKeyof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export type OmitKeyof<
TObject,
TKey extends TStrictly extends 'safely'
?
| keyof TObject
| (string & Record<never, never>)
| (number & Record<never, never>)
| (symbol & Record<never, never>)
: keyof TObject,
TStrictly extends 'strictly' | 'safely' = 'strictly',
> = Omit<TObject, TKey>
2 changes: 1 addition & 1 deletion packages/react/src/utility-types/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type { PropsWithDevMode } from './PropsWithDevMode'
export type { OmitKeyOf } from './OmitKeyOf'
export type { OmitKeyof } from './OmitKeyof'
export type { ConstructorType } from './ConstructorType'
export type { Nullable } from './Nullable'

0 comments on commit 33fe78c

Please sign in to comment.