-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Strongly Typed Fields #1334
Comments
I think Lenses could be an interesting fit for this problem (Simply put, a Lens is kind of a pointer to some field inside a data structure: a pair Getter/Setter) tldr; say we have the following data structure interface Address {
street: number;
city: string;
}
interface State {
name: string;
age: number;
addresses: Address[];
}; Instead of having <Field name="addresses[0].street" /> We'll have the following syntax <Field name={_.addresses[0].street} /> With the difference that the 2nd one is typeable (and can leverage auto completion) A sample implementation can be found here https://codesandbox.io/s/x94o6y2q3q . It uses Proxies to leverage dot notation syntax. In the case of Formik the root lens ( Another benefit could be focusing, for example you can provide a feature to focus on nested forms and allows addressing by relative paths <FormField name={_.addresses[0]}>
({ _, ... }) => <Field name={_.street}>
</FormField> |
Not to lure away, but primarily to suggest potential APIs, you might want to check out how @ForbesLindesay has strongly typed the values of Final Form in React Define Form. |
Another less unobtrusive solution is to use Proxies to just construct the string path in a type-safe way. Although less flexible than Lenses you keep almost the same code (slight modifiction in getIn & setIn) https://codesandbox.io/s/v6ojj41k20 for example p = pathProxy()
// Proxy {path: ""}
p.name
// Proxy {path: "name"}
p.name.address
// Proxy {path: "name.address"}
p.name.address[0]
// Proxy {path: "name.address.0"} |
Darn, I was hoping the type checking might prevent an error like this: const lensStreet = _.addresses[0].streeeet;
// ^^^^^^^^ typo |
could probably use a better name than |
Oops. Apologies. I expected CodeSandbox to show me an error. 🤦♂️ That's awesome! |
Hi all, thanks for this valuable information! Insofar as the proxy written there, it only appears to type the I am currently using a flat proxy to achieve that, but I'm hoping to achieve similar nesting once I can prove the typing of the fields itself will work. |
I'm not familiar with the codebase but perhaps the Field props need to parameterized over the value type, maybe something like export interface FieldConfig<Values, FieldValue> {
component?:
| string
| React.ComponentType<FieldProps<Values, FieldValue>>
| React.ComponentType<void>;
validate?: ((value: FieldValue) => string | Promise<void> | undefined);
name: string | Lens<Values, FieldValue>
value?: FieldValue;
} |
@yelouafi you can already review my PR in #1336 which does something similar by strongly typing via the Field Name (not sure how you would access the name from the Value?)
Are you saying that by passing FieldPath you'd be able to determine the type of If so, that's exactly where I'm going to be going next in the PR, but I'm having trouble building this project using |
by this you can type paths like
I'd like to beleive the TS compiler would be able to infer the 2nd type parameter from the type of the used Lens but I'm not sure; |
@yelouafi I know, it was stated previously that I had only figured it out for a single level and was going to prepare the nesting at a later time. After much brain-wracking (racking?), I came up with my best attempt of nesting and inferring the types, by using nested proxies and a special accessor called It's not very clever, but so far I've got it working on #1336. I couldn't figure out a way to deduce the key / name because there is no way to concatenate string types like The api on #1336 looks like this:
I haven't tested it for array types yet. |
The latest push on #1336 solves all the problems I could find. If this API is useful to Formik, it could use further testing. There are a few small issues with it, such as Typescript not inferring that a Typed Field with an
|
Just to clarify discussion around this issue, I've moved the proxy portion of this issue to userland via https://github.com/johnrom/formik-typed This issue is now entirely about strongly typing the |
I want to point out on the topic that major enemy is Context here which wasn't designed with type declarations in mind. Since the Context needs to be created in a global scope and type of its value needs to be specified at that moment, the consumers can only get that type. Currently, for V2 it means that even if we do We can do The Other, completely alternative approach would be utilizing an own Context instance that would be properly typed. Unfortunately, that's probably not so easy task and considering it's only for typing needs, probably not worth it. |
I was under the impression that According to my knowledge, |
Well, I made my own abstraction to allow for a strong typing of fields and added other helpers also. It's opinionated, but perhaps it can inspire someone. I find regular import { FieldConfig, FieldInputProps, FieldMetaProps, useField, useFormikContext } from 'formik'
import React from 'react'
interface IOptions {
validate: TFormValidator
type: string
}
export type TFormValidator = FieldConfig['validate']
interface FieldControlProps<TValue> extends FieldMetaProps<TValue> {
setValue: (value: TValue) => void
setTouched: (touched?: boolean) => void
hasError: boolean
}
export function useFormField<TValue = unknown>(
name: FieldName,
{ type, validate }: Partial<IOptions> = {},
) {
const formik = useFormikContext()
// https://github.com/jaredpalmer/formik/issues/1653
const finalName = Array.isArray(name) ? name.join('.') : name
// https://github.com/jaredpalmer/formik/issues/1650
React.useEffect(() => {
if (validate) {
formik.registerField(finalName, { validate } as any)
}
return () => formik.unregisterField(finalName)
}, [finalName, formik, validate])
const [field, meta] = useField({ type, name: finalName })
const control = {
...meta,
setValue(value) {
formik.setFieldValue(finalName as never, value)
},
setTouched(touched: boolean = true) {
formik.setFieldTouched(finalName as never, touched)
},
get hasError() {
return Boolean(meta.error)
},
}
// prettier-ignore
return [field, control] as [FieldInputProps<TValue>, FieldControlProps<TValue>]
} I wonder if it's possible to type correctly that returning tuple without duplicating declaration with interface. The |
When I rebase my PR over v2, I hope to solve the issues you're describing with |
I currently "fix" the typing for Field in my project like this: import {Field} from "formik";
import * as React from 'react';
type Props<TComponentProps> = {
name: string,
component: React.ComponentType<TComponentProps>
} & Omit<TComponentProps, 'name' | 'component' | 'field' | 'form'>;
class FormikField<T> extends React.PureComponent<Props<T>> {
render() {
const {name, component, ...props} = this.props;
return (
<Field name={name} component={component} {...props} />
);
}
}
export default FormikField; Then I use FormikField component, instead of directly Field. Not sure if it is perfect, but the checking worked for me correctly. Using TS 3.5.x and formik 1.5.x. I think the generic inference on components in JSX context was not always a thing in TS, therefore it was not typed in formik in first place? |
@akomm I am sorry, I couldn't understand. I am new to typescript. |
For the more general use case (
Something like:
e: actually, I suppose you could pass down the <Formik initialValues={myValues}>
{ ({ Field }) => <Field name="name.first" /> }
</Formik> |
I'm not gonna lie, TypeScript cannot handle this at all, at least not in Chrome... However, if you wait about 10 seconds, the types do infer correctly! Hopefully performance improves before release. I don't think the field name is intellisense'd. I think it only offers autosuggestions because the tokens exist in the file. |
It would be so dope to solve this. It would bring insane type safety to Formik. |
Here's another attempt using an example given in the PR I linked. I'm sure there are edge cases (it doesn't work with arrays, for one) but it seems performant. |
And it just keeps getting better: super basic array support I'd like to actually support {
"form.name": "hello",
"form.values": {
"field1": "value",
}
} |
I wrote a rudimentary typed |
I think it really comes down to -- we'll probably merge this in the next Major version since it requires new TypeScript functionality that didn't exist before. If you have any experiments you'd like to share, especially with the possibilities of Partial Inference mentioned above, I'm sure it'd be super useful when it comes to making Formik v3 |
Thanks to @millsp's work with First, a set of TS rules to make a path getter string: // object-path.ts
import {
A,
F,
B,
O,
I,
L,
M
} from 'ts-toolbelt';
type OnlyUserKeys<O> =
O extends L.List
? keyof O & number
: O extends M.BuiltInObject
? never
: keyof O & (string | number)
type PathsDot<O, I extends I.Iteration = I.IterationOf<'0'>> =
9 extends I.Pos<I> ? never :
O extends object
? {[K in keyof O & OnlyUserKeys<O>]:
`${K}` | `${K}.${PathsDot<O[K], I.Next<I>>}`
}[keyof O & OnlyUserKeys<O>]
: never
type PathDot<O, P extends string, strict extends B.Boolean = 1> =
P extends `${infer K}.${infer Rest}`
? PathDot<O.At<O & {}, K, strict>, Rest, strict>
: O.At<O & {}, P, strict>;
type ObjectPathDotted<Obj extends object, Path extends string = string> = A.Cast<Path, PathsDot<Obj>> Now that that's out of the way, we can create a field wrapper: type Props<Model extends object, Path extends string> = {
name: ObjectPathDotted<Model, Path>
}
function TextField<Model extends object, Path extends string = string>({ name }: Props) {
const [{ value }] = useFieldFast(name as string)
// render here
} Then, in my form: type Model = {
user: {
role: 'admin' | 'buyer' | 'seller'
}
}
export default Form() {
return (
<Formik>
<TextField<Model> name="user.role" />
</Formik>
)
} The result looks something like this (the above is pseudocode; here's an actual screenshot from my app where I changed the TS type to match it): This is still a work-in-progress, so I recommend following millsp/ts-toolbelt#154 to stay up to date. |
@nandorojo I believe I achieved full typings here in a way that we can implement in Formik once TypeScript 4 (or whatever newer version I used) has widespread adoption. Plus, no added dependencies. When I say full typings, I mean you can do return <Field<Model> name="user.role" value={1} /> // oh no, value should be 'admin' | 'buyer' | 'seller' Kudos for the wrapper! |
@johnrom that looks awesome. The TS getter is very elegant. One question: for |
@nandorojo hmm I don't remember exactly. I believe it was a kind of trick because Path extends string therefore string cannot extend Path, which helps the inference. |
👋 @johnrom Do we need breaking changes to make |
They will most likely be breaking changes, though by putting the generics in an illogical order I believe I was able to implement backwards compatibility in one of my PRs. It comes at the cost of dev experience though, so I'd almost rather break things now than be locked in, maybe with a quick code mod to use a fallback for users who can't update their code right away. |
Thanks! I think I formulated my question badly there too... I wasn't trying to discourage making breaking changes, I was thinking about the possibility of making a PR but now I realize it's harder than that. |
I have a PR here: #1336 (v1) and #2655 (v2) which do a significant amount of heavy lifting towards strengthening these types, but I never ended up getting 100% of the way there and have been focused on #2931 in the limited time I have to dedicate to Formik. I probably wouldn't get a full PR in until v3 is finalized, so you're free to use one or both of those as a starting point. I would keep in mind that strengthening these types means way more than The problem with opening a PR for just setFieldValue at this time, is that the foundation of what we would ideally do, and the quick fix, are going to be (as far as I can tell) completely incompatible: // What we want
type SetFieldValueFn<Values, Path extends PathOf<Values>> = (field: Path, value: Values[Path], shouldValidate?: boolean) => void;
// The shortcut:
type SetFieldValueFn<Value> = (field: string, value: Value, shouldValidate?: boolean) => void; |
Looks like we can try using microsoft/TypeScript#26568 |
Good job so far on this topic so far. Formik is great, but it would be even greater if type safety was developed further. Looking forward to this feature. |
any updates on progress here? I created a wrapper based on the @johnrom example above. Still a WIP, but seems to work for my use cases import React from 'react';
import {
Formik,
FormikErrors,
FormikHelpers,
isFunction,
FastField,
} from 'formik';
export interface TypeFormChildrenProps<T> {
Field: TypedFieldComponent<T>;
}
type Children<Values> = (
props: TypeFormChildrenProps<Values>
) => React.ReactElement;
interface TypedFormik<Values> {
initialValues: Values;
children: Children<Values>;
validate?: (values: Values) => void | Promise<FormikErrors<Values>>;
onSubmit: (
values: Values,
formikHelpers: FormikHelpers<Values>
) => void | Promise<any>;
className?: string;
}
export default function TypedFormik<Values>({
children,
...props
}: TypedFormik<Values>) {
return (
<Formik<Values> {...props}>
{({
handleSubmit,
}) => (
<form onSubmit={handleSubmit}>
{isFunction(children)
? children({ Field: FastField as any }) // todo: provide means to configure Field component.
: children}
</form>
)}
</Formik>
);
}
// Modified From: https://github.com/jaredpalmer/formik/issues/1334
// A similar approach may be PR'd into Formik soon.
// Tried to use ts-toolbelt "AutoField" to recreate, but ran into recursive limit
/** Used to create Components that Accept the TypedFieldComponent */
type MatchField<T, Path extends string> = string extends Path
? unknown
: T extends readonly unknown[]
? Path extends `${infer K}.${infer R}`
? MatchField<T[number], R>
: T[number]
: Path extends keyof T
? T[Path]
: Path extends `${infer K}.${infer R}`
? K extends keyof T
? MatchField<T[K], R>
: never
: never;
type ValidateField<T, Path extends string> = MatchField<T, Path> extends never
? never
: Path;
type StronglyTypedFieldProps<FormValues, Name extends string> = {
name: ValidateField<FormValues, Name>;
onChange?: (value: MatchField<FormValues, Name>) => void;
value?: MatchField<FormValues, Name>;
as?: any;
} & Record<string, any>;
type TypedFieldComponent<Values> = <FieldName extends string>(
props: StronglyTypedFieldProps<Values, FieldName>
) => JSX.Element | null; Usage is the same as his example interface MyFormValues {
name: {
first: string;
last: string;
suffix: "Mr." | "Mrs." | "Ms." | '';
},
age: number | '';
friends: {
name: string,
}[]
}
const initialValues: MyFormValues = {
name: {
first: '',
last: '',
suffix: '',
},
age: '',
friends: [
{
name: '',
}
]
};
const MyForm = <TypedFormik initialValues={initialValues} onSubmit={console.log}>
{({ Field }) => <>
<Field name="name.suffix" onChange={value => value === "Mr."} />
<Field name="age" onChange={value => value === 1} />
<Field name="friends.1" onChange={value => value.name === "john"} />
<Field name="friends.1.name" onChange={value => value === "john"} />
<Field name="hmmm" onChange={value => value === 1} />
</>
}
</TypedFormik> |
@pabloimrik17 this project is dead as so far as I can tell, but I was able to get this working entirely using typescript template string literals in my PR that should be linked somewhere above. The method is very complicated. This is how it works: Last time I checked, react hook form accomplished this without template literal strings simply by supporting up to a certain depth. You can check out how they did it for inspiration (or just use that library). Edit: it looks like they switched to something similar (and cleaner), though I haven't used it to tell how well it works. https://github.com/react-hook-form/react-hook-form/blob/master/src/types/path/eager.ts |
🚀 Feature request
Current Behavior
Currently, Fields are just generic magic black boxes where
any
thing goes. When I started implementing a solution using this framework, I did so on the basis that the documentation has an area for Typescript and says Typescript is supported, without any caveats: https://jaredpalmer.com/formik/docs/guides/typescriptHowever, the point of using TypeScript is to make sure mistakes are gone from the project. Inputs are properly named, values are the correct type, etc. So, I'd like to kick off another issue where I propose a real typed API for fields. There are other issues that have been closed: #768 , #673 , etc related to it, but they all say something like "This PR is tracking this issue" -- and then the PR was closed without resolution.
Desired Behavior
Edit: I've removed my initial description of this change to remove a Proxy implementation that I've moved here: https://github.com/johnrom/formik-typed
Create an opt-in framework for properly typing fields, while allowing users to continue with the "magic black-box field" system that exists for backwards compatibility and the ability to use fields in unforeseeable circumstances.
API usage:
Who does this impact? Who is this for?
TypeScript users.
The text was updated successfully, but these errors were encountered: