Open johnrom opened 5 years ago
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 (_
in the example) can be injected in the context and passed as argument in render props.
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 LensProxy
for error messages, something like Reference
or FieldPath
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 name
(and very elegantly), unless you are suggesting there is a better way to infer the Field type (think value
, validate
, handleChange
) from the path alone, as an alternative to what I've started in #1336.
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?)
FieldConfig<FormValues, FieldName extends keyof FormValues>
Are you saying that by passing FieldPath you'd be able to determine the type of value
from Lens<FormValues, FieldPath>
where FieldPath
would equal something like addresses.street
?
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 yarn start
when developing it.
FieldConfig<FormValues, FieldName extends keyof FormValues>
by this you can type paths like "name"
but not deep paths like "address.street"
Are you saying that by passing FieldPath you'd be able to determine the type of
value
fromLens<FormValues, FieldPath>
whereFieldPath
would equal something likeaddresses.street
?
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 _field_
.
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 type path = typeof parent + "." + typeof child
.
The api on #1336 looks like this:
interface Contact {
firstName: string;
lastName: string;
address: {
city: string;
state: string;
zip: number;
}
}
() => <Formik<MyValues> initialValues={selectInitialValues()} />
{({ Fields }) => (
<Form>
// `name` is automatically hooked up
<Fields.firstName._field_ validate={(value: string) => true} />
<Fields.address.zip._field_ validate={(value: number) => true} />
</Form>
)}
</Formik>
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 enum
value matches another Typed Field with the same enum value, which I think would be part of a separate issue since it appears to be related to typescript inference rather than a real issue. It also supports array fields.
interface Address {
street: {
line1: string;
line2: string;
},
zip: number;
}
interface Contact {
firstName: string;
lastName: string;
address: Address;
addresses: Address[];
}
() => <Formik<MyValues> initialValues={selectInitialValues()} />
{({ Fields }) => {
const Address0 = Fields.addresses[0];
return (<Form>
// `name` is automatically hooked up
// `value` is strongly typed
<Fields.firstName._field validate={(value: string) => true} />
<Fields.address.street.line1._field validate={(value: string) => true} />
<Address0.street.line1._field validate=(value: string) => true} />
<Address0.zip._field validate=(value: number) => true} />
</Form>)
}}
</Formik>
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 Field
class, as that is the only thing required to enable third party solutions like the proxy above.
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 useFormik<FormValues>
, it has correct types only for immediate consumption, but any subsequent access to the context value will always come to as FormikContext<any>
.
We can do useFormikContext<FormValues>
to get strongly typed context. It would be probably better if it would be required generic argument. At this moment it's easy to forget about it and get an untyped code.
The useField
is a much bigger disaster though as in current RC (v2.0.1-rc.8) the generic argument is used for an actual input prop and not the value of the field. It would be really nice if we could do useField<FormValues["someField"]>
(it's possible to access interface members like that) and get properly typed value
out of it.
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 useFormik
is not going to be a real user use-case given previous comments by @jaredpalmer (haven't had much time to spend here though, so that may have changed in later RCs). Either way, this PR isn't about typing Formik, it's about typing the fields themselves.
According to my knowledge, useFormikContext
is the recommended approach of getting Formik's context, and if someone gets the wrong result from it because they passed the wrong (or no) generic parameter... well, you might be warned if you make any incompatible assignments, but this isn't something that TS is meant to fix. You would notice when you didn't get magic autocompletion. If I rebased #1336 over v2, I believe what you propose would work (useField<FormValues["someField"]>
) (API might be a little different, like useField<FormValues, FormValues["someField"], ExtraProps>()
). It's been a minute since I opened the PR. I haven't had a chance to rebase the PR and set up hooks, and also I've been waiting for things to get a bit more formalized before knocking at it.
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 useField
fairly cumbersome in some cases. It's clearly meant for simple input-like fields.
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 typeof control
does not carry over the generic TValue
When I rebase my PR over v2, I hope to solve the issues you're describing with useField()
. I haven't had a chance to look at the v2 API in too much detail just yet. I'll definitely think about some of your ideas above in terms of solving field typing issues.
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?
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 Can you please explain how does it work?
I am sorry, I couldn't understand. I am new to typescript.
workaround I use currently, in case someone needs..
type Props<TData> = {
name: keyof TData;
} & Omit<FieldConfig, 'name'>;
function FormikField<T>({name, ...props}: Props<T>){
return <Field name={name} {...props} />;
}
Template literal types in TS 4.1 could solve this issue
@tpict nice!
As usual my brain melts when looking at TS pull requests, but with template literal types we should be able to implement a useField
like:
interface FormShape {
name: {
first: string;
}
}
const formik = useFormik<FormShape>();
const field: {
value: string,
onChange: (val: string): void,
} = formik.useField("name.first" || {
name: "name.first"
});
For the more general use case (Field
component etc) I think we would still need something like partial inference for it to be practical because:
name
prop–concretely generating the union of all valid lookups probably won't workSomething like:
type ValidLodashGetSyntax<Shape, Get extends string> = someComplicatedConditional ? Get : never;
function Field<FormShape, Name extends string>(
props: { name: ValidLodashGetSyntax<FormShape, Name> }
): React.ReactNode;
...
<Formik>
<Field<MyForm, **> name="name.first" />
</Formik>
e: actually, I suppose you could pass down the Field
component itself as a render prop through which you could apply Formik's generic type parameter, and by that means get type inference back:
<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 friends[1][name]
as well, but didn't above since I don't believe it is part of the Formik API. I think looking forward a few versions, moving to this naming convention would both help support semantic html form naming (if I choose not to hydrate an SSR React app, I'd still like user to be able to submit!) and also resolve issues people are having where they want a form definition like this:
{
"form.name": "hello",
"form.values": {
"field1": "value",
}
}
I wrote a rudimentary typed useField
hook using generics before I found this issue. Admittedly mine isn't as flexible as what @johnrom and @tpict came up with, specifically the nested field and array notation. Is there still more work to do here? I was going to contribute but I noticed there's a few open PRs, i.e. 1336, 2655.
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 ts-toolbelt
(and help here), my app has strictly-named formik name
fields using TS 4.1. It's pretty awesome.
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 https://github.com/millsp/ts-toolbelt/issues/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 MatchField
, the first thing you do is set it to unknown
if string extends Path
. Why is that?
@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.
:wave: @johnrom Do we need breaking changes to make setFieldValue
infer the value
type from the field
string too? It's something I miss from hook-form
https://github.com/formium/formik/blob/7f27ae414d35c99a4f1e540584a8a872fd0debc5/packages/formik/src/types.tsx#L94-L95
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 [key in keyof Values]: Values[key]
, as many have gone down that road before.
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 https://github.com/microsoft/TypeScript/pull/26568 package.json::typesVersions
to map a different version of FieldValue and FieldName which would map to the old keyof keyof keyof keyof
, or any
where applicable, in the event of <4.1 and map to the new string template literal in >=4.1.
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>
Hi guys,
any update on this?
Im trying to achieve some typesafe on my "name" form input components props.
So far im only able to get the keys (first level) of the form object type
But would be great nested an array index syntax support.
@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.