Skip to content

Revamp useForm's generic types across adaptors #2335

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 68 additions & 24 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,83 @@ declare module 'axios' {
}
}

export type Errors = Record<string, string>
export type DefaultInertiaConfig = {
errorValueType: string
}
/**
* Designed to allow overriding of some core types using TypeScript
* interface declaration merging.
*
* @see {@link DefaultInertiaConfig} for keys to override
* @example
* ```ts
* declare module '@inertiajs/core' {
* export interface InertiaConfig {
* errorValueType: string[]
* }
* }
* ```
*/
export interface InertiaConfig {}
export type InertiaConfigFor<Key extends keyof DefaultInertiaConfig> = Key extends keyof InertiaConfig
? InertiaConfig[Key]
: DefaultInertiaConfig[Key]
export type ErrorValue = InertiaConfigFor<'errorValueType'>

export type Errors = Record<string, ErrorValue>
export type ErrorBag = Record<string, Errors>

export type FormDataConvertibleValue = Blob | FormDataEntryValue | Date | boolean | number | null | undefined
export type FormDataConvertible =
| Array<FormDataConvertible>
| { [key: string]: FormDataConvertible }
| Blob
| FormDataEntryValue
| Date
| boolean
| number
| null
| undefined

export type FormDataKeys<T extends Record<any, any>> = T extends T
? keyof T extends infer Key extends Extract<keyof T, string>
? Key extends Key
? T[Key] extends Record<any, any>
? `${Key}.${FormDataKeys<T[Key]>}` | Key
: Key
: never
: never
: never
| FormDataConvertibleValue

export type FormDataType<T extends object> = {
[K in keyof T]: T[K] extends FormDataConvertibleValue
? T[K]
: T[K] extends (...args: unknown[]) => unknown
? never
: T[K] extends object | Array<unknown>
? FormDataType<T[K]>
: never
}

export type FormDataValues<T extends Record<any, any>, K extends FormDataKeys<T>> = K extends `${infer P}.${infer Rest}`
? P extends keyof T
? Rest extends FormDataKeys<T[P]>
? FormDataValues<T[P], Rest>
export type FormDataKeys<T> = T extends Function | FormDataConvertibleValue
? never
: T extends Array<unknown>
? number extends T['length']
? `${number}` | `${number}.${FormDataKeys<T[number]>}`
:
| Extract<keyof T, `${number}`>
| {
[Key in Extract<keyof T, `${number}`>]: `${Key & string}.${FormDataKeys<T[Key & string]> & string}`
}[Extract<keyof T, `${number}`>]
:
| Extract<keyof T, string>
| {
[Key in Extract<keyof T, string>]: `${Key}.${FormDataKeys<T[Key]> & string}`
}[Extract<keyof T, string>]

export type FormDataValues<T, K extends FormDataKeys<T>> = K extends `${infer P}.${infer Rest}`
? T extends unknown[]
? P extends `${infer I extends number}`
? Rest extends FormDataKeys<T[I]>
? FormDataValues<T[I], Rest>
: never
: never
: P extends keyof T
? Rest extends FormDataKeys<T[P]>
? FormDataValues<T[P], Rest>
: never
: never
: never
: K extends keyof T
? T[K]
: never
: T extends unknown[]
? T[K & number]
: never

export type FormDataError<T> = Partial<Record<FormDataKeys<T>, ErrorValue>>

export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete'

Expand Down
35 changes: 17 additions & 18 deletions packages/react/src/useForm.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
FormDataConvertible,
ErrorValue,
FormDataError,
FormDataKeys,
FormDataType,
FormDataValues,
Method,
Progress,
Expand All @@ -14,14 +16,13 @@ import useRemember from './useRemember'

type SetDataByObject<TForm> = (data: TForm) => void
type SetDataByMethod<TForm> = (data: (previousData: TForm) => TForm) => void
type SetDataByKeyValuePair<TForm extends Record<any, any>> = <K extends FormDataKeys<TForm>>(key: K, value: FormDataValues<TForm, K>) => void
type FormDataType = Record<string, FormDataConvertible>
type SetDataByKeyValuePair<TForm> = <K extends FormDataKeys<TForm>>(key: K, value: FormDataValues<TForm, K>) => void
type FormOptions = Omit<VisitOptions, 'data'>

export interface InertiaFormProps<TForm extends FormDataType> {
export interface InertiaFormProps<TForm extends FormDataType<TForm>> {
data: TForm
isDirty: boolean
errors: Partial<Record<FormDataKeys<TForm>, string>>
errors: FormDataError<TForm>
hasErrors: boolean
processing: boolean
progress: Progress | null
Expand All @@ -30,12 +31,12 @@ export interface InertiaFormProps<TForm extends FormDataType> {
setData: SetDataByObject<TForm> & SetDataByMethod<TForm> & SetDataByKeyValuePair<TForm>
transform: (callback: (data: TForm) => object) => void
setDefaults(): void
setDefaults(field: FormDataKeys<TForm>, value: FormDataConvertible): void
setDefaults<T extends FormDataKeys<TForm>>(field: T, value: FormDataValues<TForm, T>): void
setDefaults(fields: Partial<TForm>): void
reset: (...fields: FormDataKeys<TForm>[]) => void
clearErrors: (...fields: FormDataKeys<TForm>[]) => void
setError(field: FormDataKeys<TForm>, value: string): void
setError(errors: Record<FormDataKeys<TForm>, string>): void
setError(field: FormDataKeys<TForm>, value: ErrorValue): void
setError(errors: FormDataError<TForm>): void
submit: (...args: [Method, string, FormOptions?] | [{ url: string; method: Method }, FormOptions?]) => void
get: (url: string, options?: FormOptions) => void
patch: (url: string, options?: FormOptions) => void
Expand All @@ -44,12 +45,12 @@ export interface InertiaFormProps<TForm extends FormDataType> {
delete: (url: string, options?: FormOptions) => void
cancel: () => void
}
export default function useForm<TForm extends FormDataType>(initialValues?: TForm): InertiaFormProps<TForm>
export default function useForm<TForm extends FormDataType>(
export default function useForm<TForm extends FormDataType<TForm>>(initialValues?: TForm): InertiaFormProps<TForm>
export default function useForm<TForm extends FormDataType<TForm>>(
rememberKey: string,
initialValues?: TForm,
): InertiaFormProps<TForm>
export default function useForm<TForm extends FormDataType>(
export default function useForm<TForm extends FormDataType<TForm>>(
rememberKeyOrInitialValues?: string | TForm,
maybeInitialValues?: TForm,
): InertiaFormProps<TForm> {
Expand All @@ -62,8 +63,8 @@ export default function useForm<TForm extends FormDataType>(
const recentlySuccessfulTimeoutId = useRef(null)
const [data, setData] = rememberKey ? useRemember(defaults, `${rememberKey}:data`) : useState(defaults)
const [errors, setErrors] = rememberKey
? useRemember({} as Partial<Record<FormDataKeys<TForm>, string>>, `${rememberKey}:errors`)
: useState({} as Partial<Record<FormDataKeys<TForm>, string>>)
? useRemember({} as FormDataError<TForm>, `${rememberKey}:errors`)
: useState({} as FormDataError<TForm>)
const [hasErrors, setHasErrors] = useState(false)
const [processing, setProcessing] = useState(false)
const [progress, setProgress] = useState(null)
Expand Down Expand Up @@ -197,7 +198,7 @@ export default function useForm<TForm extends FormDataType>(
)

const setDefaultsFunction = useCallback(
(fieldOrFields?: FormDataKeys<TForm> | Partial<TForm>, maybeValue?: FormDataConvertible) => {
(fieldOrFields?: FormDataKeys<TForm> | Partial<TForm>, maybeValue?: unknown) => {
if (typeof fieldOrFields === 'undefined') {
setDefaults(() => data)
} else {
Expand Down Expand Up @@ -232,13 +233,11 @@ export default function useForm<TForm extends FormDataType>(
)

const setError = useCallback(
(fieldOrFields: FormDataKeys<TForm> | Record<FormDataKeys<TForm>, string>, maybeValue?: string) => {
(fieldOrFields: FormDataKeys<TForm> | FormDataError<TForm>, maybeValue?: string) => {
setErrors((errors) => {
const newErrors = {
...errors,
...(typeof fieldOrFields === 'string'
? { [fieldOrFields]: maybeValue }
: (fieldOrFields as Record<FormDataKeys<TForm>, string>)),
...(typeof fieldOrFields === 'string' ? { [fieldOrFields]: maybeValue } : fieldOrFields),
}
setHasErrors(Object.keys(newErrors).length > 0)
return newErrors
Expand Down
38 changes: 21 additions & 17 deletions packages/svelte/src/useForm.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type {
ActiveVisit,
Errors,
FormDataConvertible,
ErrorValue,
FormDataError,
FormDataKeys,
FormDataType,
FormDataValues,
Method,
Page,
PendingVisit,
Expand All @@ -16,28 +19,27 @@ import { cloneDeep, isEqual } from 'es-toolkit'
import { get, has, set } from 'es-toolkit/compat'
import { writable, type Writable } from 'svelte/store'

type FormDataType = Record<string, FormDataConvertible>
type FormOptions = Omit<VisitOptions, 'data'>

export interface InertiaFormProps<TForm extends FormDataType> {
export interface InertiaFormProps<TForm extends FormDataType<TForm>> {
isDirty: boolean
errors: Partial<Record<FormDataKeys<TForm>, string>>
errors: FormDataError<TForm>
hasErrors: boolean
progress: Progress | null
wasSuccessful: boolean
recentlySuccessful: boolean
processing: boolean
setStore(data: TForm): void
setStore(key: FormDataKeys<TForm>, value?: FormDataConvertible): void
setStore<T extends FormDataKeys<TForm>>(key: T, value: FormDataValues<TForm, T>): void
data(): TForm
transform(callback: (data: TForm) => object): this
defaults(): this
defaults(fields: Partial<TForm>): this
defaults(field?: FormDataKeys<TForm>, value?: FormDataConvertible): this
defaults<T extends FormDataKeys<TForm>>(field: T, value: FormDataValues<TForm, T>): this
reset(...fields: FormDataKeys<TForm>[]): this
clearErrors(...fields: FormDataKeys<TForm>[]): this
setError(field: FormDataKeys<TForm>, value: string): this
setError(errors: Errors): this
setError(field: FormDataKeys<TForm>, value: ErrorValue): this
setError(errors: FormDataError<TForm>): this
submit: (...args: [Method, string, FormOptions?] | [{ url: string; method: Method }, FormOptions?]) => void
get(url: string, options?: FormOptions): void
post(url: string, options?: FormOptions): void
Expand All @@ -47,14 +49,16 @@ export interface InertiaFormProps<TForm extends FormDataType> {
cancel(): void
}

export type InertiaForm<TForm extends FormDataType> = InertiaFormProps<TForm> & TForm
export type InertiaForm<TForm extends FormDataType<TForm>> = InertiaFormProps<TForm> & TForm

export default function useForm<TForm extends FormDataType>(data: TForm | (() => TForm)): Writable<InertiaForm<TForm>>
export default function useForm<TForm extends FormDataType>(
export default function useForm<TForm extends FormDataType<TForm>>(
data: TForm | (() => TForm),
): Writable<InertiaForm<TForm>>
export default function useForm<TForm extends FormDataType<TForm>>(
rememberKey: string,
data: TForm | (() => TForm),
): Writable<InertiaForm<TForm>>
export default function useForm<TForm extends FormDataType>(
export default function useForm<TForm extends FormDataType<TForm>>(
rememberKeyOrData: string | TForm | (() => TForm),
maybeData?: TForm | (() => TForm),
): Writable<InertiaForm<TForm>> {
Expand All @@ -78,21 +82,21 @@ export default function useForm<TForm extends FormDataType>(
wasSuccessful: false,
recentlySuccessful: false,
processing: false,
setStore(keyOrData, maybeValue = undefined) {
setStore(keyOrData: keyof InertiaFormProps<TForm> | FormDataKeys<TForm> | TForm, maybeValue = undefined) {
store.update((store) => {
return typeof keyOrData === 'string' ? set(store, keyOrData, maybeValue) : Object.assign(store, keyOrData)
})
},
data() {
return Object.keys(data).reduce((carry, key) => {
return set(carry, key, get(this, key))
}, {} as FormDataType) as TForm
}, {} as TForm)
},
transform(callback) {
transform = callback
return this
},
defaults(fieldOrFields?: FormDataKeys<TForm> | Partial<TForm>, maybeValue?: FormDataConvertible) {
defaults(fieldOrFields?: FormDataKeys<TForm> | Partial<TForm>, maybeValue?: unknown) {
if (typeof fieldOrFields === 'undefined') {
defaults = cloneDeep(this.data())
} else {
Expand All @@ -114,13 +118,13 @@ export default function useForm<TForm extends FormDataType>(
.filter((key) => has(clonedData, key))
.reduce((carry, key) => {
return set(carry, key, get(clonedData, key))
}, {} as FormDataType) as TForm,
}, {} as TForm),
)
}

return this
},
setError(fieldOrFields: FormDataKeys<TForm> | Errors, maybeValue?: string) {
setError(fieldOrFields: FormDataKeys<TForm> | FormDataError<TForm>, maybeValue?: ErrorValue) {
this.setStore('errors', {
...this.errors,
...((typeof fieldOrFields === 'string' ? { [fieldOrFields]: maybeValue } : fieldOrFields) as Errors),
Expand Down
35 changes: 22 additions & 13 deletions packages/vue3/src/useForm.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { FormDataConvertible, FormDataKeys, Method, Progress, router, VisitOptions } from '@inertiajs/core'
import {
ErrorValue,
FormDataError,
FormDataKeys,
FormDataType,
FormDataValues,
Method,
Progress,
router,
VisitOptions,
} from '@inertiajs/core'
import { cloneDeep, isEqual } from 'es-toolkit'
import { get, has, set } from 'es-toolkit/compat'
import { reactive, watch } from 'vue'

type FormDataType = Record<string, FormDataConvertible>
type FormOptions = Omit<VisitOptions, 'data'>

export interface InertiaFormProps<TForm extends FormDataType> {
export interface InertiaFormProps<TForm extends FormDataType<TForm>> {
isDirty: boolean
errors: Partial<Record<FormDataKeys<TForm>, string>>
errors: FormDataError<TForm>
hasErrors: boolean
processing: boolean
progress: Progress | null
Expand All @@ -17,12 +26,12 @@ export interface InertiaFormProps<TForm extends FormDataType> {
data(): TForm
transform(callback: (data: TForm) => object): this
defaults(): this
defaults(field: FormDataKeys<TForm>, value: FormDataConvertible): this
defaults<T extends FormDataKeys<TForm>>(field: T, value: FormDataValues<TForm, T>): this
defaults(fields: Partial<TForm>): this
reset(...fields: FormDataKeys<TForm>[]): this
clearErrors(...fields: FormDataKeys<TForm>[]): this
setError(field: FormDataKeys<TForm>, value: string): this
setError(errors: Record<FormDataKeys<TForm>, string>): this
setError(field: FormDataKeys<TForm>, value: ErrorValue): this
setError(errors: Record<FormDataKeys<TForm>, ErrorValue>): this
submit: (...args: [Method, string, FormOptions?] | [{ url: string; method: Method }, FormOptions?]) => void
get(url: string, options?: FormOptions): void
post(url: string, options?: FormOptions): void
Expand All @@ -32,14 +41,14 @@ export interface InertiaFormProps<TForm extends FormDataType> {
cancel(): void
}

export type InertiaForm<TForm extends FormDataType> = TForm & InertiaFormProps<TForm>
export type InertiaForm<TForm extends FormDataType<TForm>> = TForm & InertiaFormProps<TForm>

export default function useForm<TForm extends FormDataType>(data: TForm | (() => TForm)): InertiaForm<TForm>
export default function useForm<TForm extends FormDataType>(
export default function useForm<TForm extends FormDataType<TForm>>(data: TForm | (() => TForm)): InertiaForm<TForm>
export default function useForm<TForm extends FormDataType<TForm>>(
rememberKey: string,
data: TForm | (() => TForm),
): InertiaForm<TForm>
export default function useForm<TForm extends FormDataType>(
export default function useForm<TForm extends FormDataType<TForm>>(
rememberKeyOrData: string | TForm | (() => TForm),
maybeData?: TForm | (() => TForm),
): InertiaForm<TForm> {
Expand Down Expand Up @@ -72,7 +81,7 @@ export default function useForm<TForm extends FormDataType>(

return this
},
defaults(fieldOrFields?: FormDataKeys<TForm> | Partial<TForm>, maybeValue?: FormDataConvertible) {
defaults(fieldOrFields?: FormDataKeys<TForm> | Partial<TForm>, maybeValue?: unknown) {
if (typeof data === 'function') {
throw new Error('You cannot call `defaults()` when using a function to define your form data.')
}
Expand Down Expand Up @@ -106,7 +115,7 @@ export default function useForm<TForm extends FormDataType>(

return this
},
setError(fieldOrFields: FormDataKeys<TForm> | Record<FormDataKeys<TForm>, string>, maybeValue?: string) {
setError(fieldOrFields: FormDataKeys<TForm> | FormDataError<TForm>, maybeValue?: ErrorValue) {
Object.assign(this.errors, typeof fieldOrFields === 'string' ? { [fieldOrFields]: maybeValue } : fieldOrFields)

this.hasErrors = Object.keys(this.errors).length > 0
Expand Down
Loading