Skip to content

FormData methods should take a GenericΒ #43797

Open

Description

@wesbos

Suggestion

πŸ” Search Terms

Form Data, .entries()

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

FormData entries() method, and perhaps .keys() and .values() should take a generic.

Right now it returns FormDataEntry value and is difficult type type the returned value of that Iterable.

πŸ“ƒ Motivating Example

Example of the problem here:

https://www.typescriptlang.org/play?strictNullChecks=false&target=6#code/PTAEEEGdIVwWwKagIagGYHsBOdQBsBLAayQBcALAyUAgO1AqQBEB5AWQBpQAjGU0AO4EK+ZNwR5qCUgGMAdHIBQMjLUj9MOUAF5QwAFShypOHlD6wAAwA8muAD5FoUNboAHPgwCebhNoBE6lh0AOb+oLTIiAFoBFjq4QQAJjFxCaAAbsh4MH7+AgiQ-o7OrrQe-KQ+eUGh4ZHR-njI6ckBzelZOXncGEWO1sB29pYA3IqKIKAAKuQI9HSgcF4zAMoAXBN0pAhYaMgySAAKu5CqoADeTuhppOugtbQh484ddw+kwU-jAL4TaDBaDJSARzpBkF4ABIEAAUvniqnuJwRtAAlJdrlhpDAsPRLJCECsACQXeFnWhyWLxUg-MaKP7KVTqdDYOAAUTMuiSGBk8HmpDkAEdclgvKsJAhgdgYQByOwy1HjFRqDSspjIUioXS0BACUAAMTVGuQMLsHMVkzAAGFVBldpVyBqHjAZCQsAwMCgFjssGI8AhGSrQEljQBJH1+pC6OzqzVyfnBQowi1TG20O1YB1O4S7SMer2gDDcABWkoFlpmc1AbiwRf9uBUiGocyxXHIGD13NAoaWyBIgkKKFAyPJ3l8AH5QIHmQVqLoWCWy5Ta+zaJ8CEmQ5rw7nuP6LeCobDZxaK7MqMGMPNIDL+AJsERp3fCgAmHSgBel4HLjCr9ebsMIz3BB0RaYdTlUcYz0oahuUKLheH4C9hELIgIUncUEDgahYkgcgvDkKdlRnQoAGZ30-Jc0BXNk10TSAYS3ZAd19YDQOoQEiFoDt6DAkdIMUQ9oRhWdSNPCsWEYd1ZgwGAQmMSB7grABGQjzykAAPKI3H9GhqEgAg4B0hAuB7FQYDwJJe1oGBsjwFYZCxDUyCrIsv1IBDPAoOgiBQXpPGaLAQl2QtF2BSAgA

Feeding the .entries() Iterable into Object.fromEntries(), gives us a type of { [k: string]: FormDataEntryValue; }, and has no overlap between the acutal data (Person in my case) that is supposed to come back.

So perhaps something like formData.entries<Person>() or formData.entries<IterableIterator<Person>>()?

I just know it's really hard to convert the result into a type. And the solutions are either as unknown as Person or a Type Guard, which makes me manually type each property.

πŸ’» Use Cases

See above link example

Activity

  1. max-hk commented on May 13, 2021

    @max-hk

    How about making FormData itself generic?

    So we can do something like this:

    const formData: FormData<Person> = new FormData(formEl);
  2. axetroy commented on Aug 23, 2021

    @axetroy

    I am looking forward to it, it is very useful to me.

  3. axetroy commented on Aug 23, 2021

    @axetroy

    Here is my solution: define a custom FormData

    interface IRuntimeForm {
      [key: string]: any;
    }
    
    export class RuntimeForm<T extends IRuntimeForm> {
      constructor(private _form: T) {}
      public formData(): FormData {
        const form = new FormData();
    
        for (const key in this._form) {
          if (this._form[key] !== undefined) {
            form.append(key, this._form[key]);
          }
        }
    
        return form;
      }
    }
    
    new RuntimeForm<{ foo: string }>({ foo: "bar" })
  4. sambitevidev commented on Sep 30, 2021

    @sambitevidev

    How about making FormData itself generic?

    So we can do something like this:

    const formData: FormData<Person> = new FormData(formEl);

    yes that'd be great

  5. cricketnest commented on Oct 3, 2021

    @cricketnest

    Here is my solution: define a custom FormData

    interface IRuntimeForm {
      [key: string]: any;
    }
    
    export class RuntimeForm<T extends IRuntimeForm> {
      constructor(private _form: T) {}
      public formData(): FormData {
        const form = new FormData();
    
        for (const key in this._form) {
          if (this._form[key] !== undefined) {
            form.append(key, this._form[key]);
          }
        }
    
        return form;
      }
    }
    
    new RuntimeForm<{ foo: string }>({ foo: "bar" })

    thanks, nice temp solution.
    generic form data would be a great feature still.

  6. ZinkNotTheMetal commented on Feb 22, 2022

    @ZinkNotTheMetal
    export class RuntimeForm<T extends IRuntimeForm> {
      constructor(private form: T) {}
    
      public formData(): FormData {
        const form = new FormData();
    
        Object.keys(this.form).forEach((key) => {
          if (this.form[key] !== undefined) {
            form.append(key, this.form[key])
          }
        })
    
        return form;
      }
    }

    If anyone is using AirBnB rules and wants the above to work

  7. RyKilleen commented on Mar 8, 2022

    @RyKilleen

    Just wanted to note that this'd be a very welcome feature, especially as some newer frameworks like Remix put FormData front and center.

  8. JohnCido commented on Apr 18, 2022

    @JohnCido

    I started using FormData a lot recently in a full-stack project and it's painful to use something not typed. This would be really handy.

  9. chandlervdw commented on Apr 19, 2022

    @chandlervdw
    • 1
  10. filipemir commented on Jul 6, 2022

    @filipemir

    +1 on this suggestion. Remix's reliance on FormData is making this much more important

  11. viktor-ulyankin commented on Aug 3, 2022

    @viktor-ulyankin

    +1

  12. knenkne commented on Aug 5, 2022

    @knenkne

    +1

  13. DigitalNaut commented on Aug 22, 2022

    @DigitalNaut

    +1

  14. 7 remaining items

  15. alojzy231 commented on Oct 17, 2022

    @alojzy231

    +1

  16. mohamedanwer123 commented on Oct 18, 2022

    @mohamedanwer123

    +1

  17. cdeutsch commented on Oct 18, 2022

    @cdeutsch

    Please stop with the +1's. Everyone subscribed to this issue gets a notification for it.

    Use the emoji reactions on the first post instead.

  18. DrakkoFire commented on Nov 11, 2022

    @DrakkoFire

    Any news on this ?

  19. elving commented on Nov 11, 2022

    @elving

    This is what I ended up doing:

    type TypedFormDataValue = FormDataEntryValue | Blob
    
    /**
     * Polyfill for FormData Generic
     *
     * {@link https://github.com/microsoft/TypeScript/issues/43797}
     * {@link https://xhr.spec.whatwg.org/#interface-formdata}
     */
    interface TypedFormData<T extends Record<string, TypedFormDataValue>> {
      /**
       * Appends a new value onto an existing key inside a FormData object, or adds the key if
       * it does not already exist.
       *
       * {@link https://developer.mozilla.org/en-US/docs/Web/API/FormData#formdata.append}
       */
      append<K extends keyof T>(name: K, value: T[K], fileName?: string): void
    
      /**
       * Deletes a key/value pair from a FormData object.
       *
       * {@link https://developer.mozilla.org/en-US/docs/Web/API/FormData#formdata.delete}
       */
      delete(name: keyof T): void
    
      /**
       * Returns an iterator allowing to go through all key/value pairs contained in this object.
       *
       * {@link https://developer.mozilla.org/en-US/docs/Web/API/FormData#formdata.entries}
       */
      entries<K extends keyof T>(): IterableIterator<[K, T[K]]>
    
      /**
       * Returns the first value associated with a given key from within a FormData object.
       *
       * {@link https://developer.mozilla.org/en-US/docs/Web/API/FormData#formdata.get}
       */
      get<K extends keyof T>(name: K): T[K] | null
    
      /**
       * Returns an array of all the values associated with a given key from within a FormData.
       *
       * {@link https://developer.mozilla.org/en-US/docs/Web/API/FormData#formdata.getall}
       */
      getAll<K extends keyof T>(name: K): Array<T[K]>
    
      /**
       * Returns a boolean stating whether a FormData object contains a certain key.
       *
       * {@link https://developer.mozilla.org/en-US/docs/Web/API/FormData#formdata.has}
       */
      has(name: keyof T): boolean
    
      /**
       * Returns an iterator allowing to go through all keys of the key/value pairs contained in
       * this object.
       *
       * {@link https://developer.mozilla.org/en-US/docs/Web/API/FormData#formdata.keys}
       */
      keys(): IterableIterator<keyof T>
    
      /**
       * Sets a new value for an existing key inside a FormData object, or adds the key/value
       * if it does not already exist.
       *
       * {@link https://developer.mozilla.org/en-US/docs/Web/API/FormData#formdata.set}
       */
      set(name: keyof T, value: TypedFormDataValue, fileName?: string): void
    
      /**
       * Returns an iterator allowing to go through all values contained in this object.
       *
       * {@link https://developer.mozilla.org/en-US/docs/Web/API/FormData#formdata.values}
       */
      values(): IterableIterator<T[keyof T]>
    
      forEach<K extends keyof T>(
        callbackfn: (value: T[K], key: K, parent: TypedFormData<T>) => void,
        thisArg?: unknown,
      ): void
    }
    
    function getTypedFormData<T extends Record<string, TypedFormDataValue>>(
      form?: HTMLFormElement | null,
    ): TypedFormData<T> {
      return new FormData(form || undefined) as unknown as TypedFormData<T>
    }
  20. DrakkoFire commented on Nov 11, 2022

    @DrakkoFire

    Pretty convenient, thanks Elving RodrΓ­guez (@elving) :)

  21. karlhorky commented on Feb 15, 2023

    @karlhorky
    Contributor

    I asked for a similar feature for Response and Request interfaces to accept generic type parameters: #52777

  22. karlhorky commented on Feb 20, 2023

    @karlhorky
    Contributor

    Additional use case

    A typed API client function would be a use case for this:

    type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
    
    type Options<Method> = {
      method?: Method;
    }
    
    export async function fetchApi<Path extends string, Method extends HttpMethod>(
      path: Path,
      options: Options<Method> & {
        formData: `${Method} ${Path}` extends `POST /exercise-checks`
          ? { id: string, file: File } // πŸ‘ˆ Could be FormData<{ id: string, file: File }>
          : `${Method} ${Path}` extends `PUT /exercise-checks/${number}`
          ? { file: File } // πŸ‘ˆ Could be FormData<{ file: File }>
          : never
      }): Promise<
        `${Method} ${Path}` extends `POST /exercise-checks`
          ? { id: string } | { errors: string[] }
          : `${Method} ${Path}` extends `PUT /exercise-checks/${number}`
          ? { id: string } | { errors: string[] }
          : never
      > {
      return '' as any;
    }
    
    const file = new File([''], '');
    const goodFormData = { id: '1', file: file }; // πŸ‘ˆ Could be new FormData() + .append
    const badFormData = { incorrectProperty: false }; // πŸ‘ˆ Could be new FormData() + .append
    
    // βœ… No errors
    await fetchApi('/exercise-checks', { method: 'POST', formData: goodFormData })
    // βœ… No errors
    await fetchApi('/exercise-checks/1', { method: 'PUT', formData: { file: file } })
    // βœ… Errors, incorrect method
    await fetchApi('/exercise-checks/1', { method: 'PUTzzzzz', formData: { file: file } })
    // βœ… Error, incorrect path
    await fetchApi('/xxxxx', { method: 'PUT', formData: { file: file } })
    // βœ… Error, incorrect form data
    await fetchApi('/exercise-checks/1', { method: 'POST', formData: badFormData })

    TypeScript Playground

  23. twicer-is-coder commented on Sep 24, 2023

    @twicer-is-coder

    +1

  24. adophilus commented on Oct 14, 2023

    @adophilus

    +1

  25. verber33 commented on Nov 14, 2023

    @verber33

    +1

  26. zonzujiro commented on Feb 9, 2024

    @zonzujiro

    +1

  27. ayech0x2 commented on Jun 3, 2024

    @ayech0x2

    +1

  28. k1eu commented on Aug 12, 2024

    @k1eu

    You can check out my library for just doing this thing :) Its built on the FormData primitive so can be used in replacement of native FormData :)
    https://github.com/k1eu/typed-formdata

  29. notpushkin commented on Jan 17, 2025

    @notpushkin

    Tomasz Kielar (@k1eu) Thank you so much, this is incredible!

    If anybody wants a version of this without any runtime code, it’s possible to make TypedFormData a generic interface. Here’s a simple example:

    export interface TypedFormData<T extends Record<string, string | File>> extends FormData {
      get<K extends keyof T>(key: Extract<K, string>): T[K];
    }

    (I’ve also removed the | null clause here, so that you can denote optional fields)

    type LoginFormData = TypedFormData<{
      username: string;
      password: string;
      captcha?: string;  // only shown if user is a haxxor
    }>;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions