Skip to content
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

Add renameKey and renameKeys functions to the Record module #3653

Open
sromexs opened this issue Sep 22, 2024 · 2 comments
Open

Add renameKey and renameKeys functions to the Record module #3653

sromexs opened this issue Sep 22, 2024 · 2 comments
Labels
enhancement New feature or request

Comments

@sromexs
Copy link

sromexs commented Sep 22, 2024

What is the problem this feature would solve?

Currently, the Record module lacks a built-in, type-safe method for renaming keys in an object while preserving full type information in TypeScript. Renaming object keys is a common task in data transformation, but existing approaches often lead to loss of type information or require cumbersome and error-prone workarounds. Developers need a reliable way to rename keys without sacrificing type safety or resorting to manual type definitions.

What is the feature you are proposing to solve the problem?

I propose adding two new utility functions to the Record module:

  1. renameKey: A function that renames a single key in an object.

    • Signature:
      function renameKey<
        T extends Record<string | symbol, unknown>,
        K extends keyof T,
        K2 extends string,
      >(input: T, key: K, newKey: K2): { [P in Exclude<keyof T, K> | K2]: T[P extends K2 ? K : P] };
    • Usage:
      const source = { foo: 1, bar: "baz" } as const;
      const result = renameKey(source, "foo", "yolo");
      // Resulting type:
      // { readonly yolo: 1; readonly bar: "baz"; }
  2. renameKeys: A function that renames multiple keys in an object based on a mapping.

    • Signature:
      function renameKeys<
        T extends Record<string | symbol, unknown>,
        M extends Record<string, string>,
      >(input: T, map: M): {
        [Property in keyof T as Property extends keyof M ? M[Property] : Property]: T[Property];
      };
    • Usage:
      const source = { foo: 1, bar: "baz" } as const;
      const mapping = { foo: "yolo", bar: "qux" } as const;
      const result = renameKeys(source, mapping);
      // Resulting type:
      // { readonly yolo: 1; readonly qux: "baz"; }

These functions solve the problem by:

  • Preserving Type Safety: They maintain full type information of the original object, ensuring that the returned object accurately reflects the types.
  • Convenience: Provide straightforward methods to rename one or multiple keys without manual type definitions.
  • Flexibility: renameKeys allows for multiple key renames in a single operation, while renameKey is optimized for single key renaming.

What alternatives have you considered?

Several alternatives were considered but found lacking:

  1. Manual Key Renaming with Type Casting:

    • Drawbacks:
      • Verbose and error-prone.
      • Requires manual updates to types, increasing maintenance overhead.
      • Risk of losing type information or introducing type errors.
  2. Using Existing Utility Functions or Libraries:

    • Drawbacks:
      • Third-party libraries may not handle type preservation adequately.
      • Introduces additional dependencies to the project.
      • May not align with the project's existing patterns or standards.
  3. Custom Helper Functions in Individual Projects:

    • Drawbacks:
      • Leads to code duplication across different projects.
      • Inconsistent implementations can cause confusion and bugs.
      • Lacks the optimization and testing that a standardized utility would have.
  4. Extending Types with Mapped Types Manually:

    • Drawbacks:
      • Can become complex quickly, especially with deeply nested objects.
      • Increases cognitive load for developers.
      • Not practical for dynamic key renaming based on runtime data.

These alternatives either compromise on type safety, increase complexity, or don't provide a reusable and maintainable solution. By adding renameKey and renameKeys to the Record module, we offer a robust, type-safe, and developer-friendly solution to a common problem.

@sromexs sromexs added the enhancement New feature or request label Sep 22, 2024
@sromexs sromexs changed the title Title: Add renameKey and renameKeys functions to the Record module Add renameKey and renameKeys functions to the Record module Sep 22, 2024
@fnimick
Copy link

fnimick commented Sep 22, 2024

EDIT: cleaned up the return type signatures

export function renameKeys<
  T extends Record<string | symbol, unknown>,
  M extends Record<string, string>,
>(input: T, map: M): { [Key in keyof T as Key extends keyof M ? M[Key] : Key]: T[Key] } {
  return Record.mapKeys(input, (key) => (key in map ? map[key as keyof M] : key)) as any;
}

export function renameKey<
  T extends Record<string | symbol, unknown>,
  K extends keyof T,
  K2 extends string,
>(input: T, key: K, newKey: K2): { [Key in keyof T as Key extends K ? K2 : Key]: T[Key] } {
  return renameKeys(input, { [key]: newKey } as Record<K, K2>) as any;
}

@fnimick
Copy link

fnimick commented Sep 22, 2024

Hm, the types here aren't as nice as I'd like when there is a collision in renaming.

As an example:

  test("name overrides", () => {
    const obj = { a: 1, b: 2 } as const;
    const res = renameKeys(obj, { a: "b" } as const);
    expect(res).toEqual({ b: 2 }); // type is inferred as { b: 1 | 2 }
  });
  test("name overrides 2", () => {
    const obj = { a: 1, b: 2 } as const;
    const res = renameKeys(obj, { b: "a" } as const);
    expect(res).toEqual({ a: 2 }); // type is inferred as { a: 1 | 2 }
  });

Unfortunately, there's no way around this as the types are evaluated at compile time, but you could easily reorder the entries in the object at runtime so there's a different evaluation order in the Record.mapKeys call and therefore get a different runtime value result.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants