Skip to content

Conversation

Harris-Miller
Copy link
Collaborator

Types for new function added in ramda/ramda#3493

@RobinTail
Copy link
Contributor

RobinTail commented Jun 27, 2025

https://stackoverflow.com/questions/55454125/typescript-remapping-object-properties-in-typesafe

For the needs of my project I came up with this compilation.
Perhaps it could be useful, @Harris-Miller

type TuplesFromObject<T> = {
  [P in keyof T]: [P, T[P]];
}[keyof T];

type GetKeyByValue<T, V> =
  TuplesFromObject<T> extends infer TT
    ? TT extends [infer P, V]
      ? P
      : never
    : never;

export type Remap<T, U extends { [P in keyof T]?: V }, V extends string> = {
  [P in NonNullable<U[keyof U]>]: T[GetKeyByValue<U, P>];
};

export type Intact<T, U> = { [K in Exclude<keyof T, keyof U>]: T[K] };

@Harris-Miller
Copy link
Collaborator Author

Ugh ok this one sucks

@RobinTail thanks for the suggestion, that worked great for renameKeys. Because of how that function takes an object who's type can be deconstructed into a tuple of key-value-pairs, we can extract the new key values to replace the incoming object with

That isn't really possible for rebuild and mapKeys. Those are functions return either entry tuples or dynamic keys without knowing what the original key was it was built from. This means for any object that doesn't have consistent value types breakdown hard

For now, I'm choosing to have those both return Record<string, T> until I can figure something better. That type is the baseline that should allow it to be assigned to predefined types without needing to cast. I might need to test that more first before merge

@Harris-Miller
Copy link
Collaborator Author

Actually, I may update this MR to just add renameKeys and leave the others for another MR. I can release multiple patches for @types/ramda without issue

@Harris-Miller Harris-Miller force-pushed the types-for-rebuild-mapKeys-renameKeys branch from 14a24a4 to 5a0aded Compare July 25, 2025 05:14
@Harris-Miller Harris-Miller changed the title new(rebuild, mapKeys, renameKeys) add types for new functions new(rebuild, mapKeys) add types for new functions Jul 25, 2025
@Harris-Miller
Copy link
Collaborator Author

Harris-Miller commented Jul 25, 2025

I cherry-picked over renameKeys into the now released [email protected]. What remains in the MR is rebuild and mapKeys which are not yet completed.

@RobinTail feel free to contribute directly to this PR, I honestly am not going to have the time until at least September to work on this more

@RobinTail
Copy link
Contributor

Ok, I will take a look, @Harris-Miller

@RobinTail
Copy link
Contributor

RobinTail commented Jul 25, 2025

@RobinTail feel free to contribute directly to this PR

@Harris-Miller , I don't have permissions to contribute

ERROR: Permission to ramda/types.git denied to RobinTail.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

But here is my proposition on improving the overloads having all arguments!
If you'd invite me to collaborate or somehow grant me permissions, I could push it directly:

Date: Sat, 26 Jul 2025 00:40:04 +0200
Subject: [PATCH] Improving overloads having all arguments, test for mapKeys.

---
 test/mapKeys.test.ts | 19 +++++++++++++++++++
 test/rebuild.test.ts |  8 ++++----
 types/mapKeys.d.ts   |  2 +-
 types/rebuild.d.ts   |  2 +-
 4 files changed, 25 insertions(+), 6 deletions(-)
 create mode 100644 test/mapKeys.test.ts

diff --git a/test/mapKeys.test.ts b/test/mapKeys.test.ts
new file mode 100644
index 0000000..dbac7c2
--- /dev/null
+++ b/test/mapKeys.test.ts
@@ -0,0 +1,19 @@
+import { expectType } from 'tsd';
+
+import { mapKeys } from '../es';
+
+const subject1 = { foo: '123-456', bar: '678' };
+
+// poorly typed function
+const result1 = mapKeys((key) => key.toUpperCase(), subject1);
+expectType<Record<string, string>>(result1);
+
+// well typed function
+const result2 = mapKeys((key) => key.toUpperCase() as Uppercase<typeof key>, subject1);
+expectType<Record<'FOO' | 'BAR', string>>(result2);
+
+// different value types
+const subject2 = { foo: 123, bar: 'blah' };
+
+const result3 = mapKeys((key) => key.toUpperCase() as Uppercase<typeof key>, subject2);
+expectType<Record<'FOO' | 'BAR', string | number>>(result3);
diff --git a/test/rebuild.test.ts b/test/rebuild.test.ts
index ce0be71..86f864b 100644
--- a/test/rebuild.test.ts
+++ b/test/rebuild.test.ts
@@ -8,22 +8,22 @@ const newObj = rebuild(([k, v]) => {
   return v.split('-').map((n, i) => [`${k}${i}`, n]);
 }, oldObj);

-expectType<Record<string, string>>(newObj);
+expectType<Record<`foo${number}` | `bar${number}`, string>>(newObj);

 const newObj2 = rebuild(([k, v]) => {
   return [[k, v.split('-')]];
 }, oldObj);

-expectType<Record<string, string[]>>(newObj2);
+expectType<Record<'foo' | 'bar', string[]>>(newObj2);

 const newObj3 = rebuild(([k, v]) => {
   const innerObj = Object.fromEntries(v.split('-').map((n, i) => [i, n]));
   return [[k, innerObj]];
 }, oldObj);

-expectType<Record<string, Record<string, string>>>(newObj3);
+expectType<Record<'foo' | 'bar', Record<string, string>>>(newObj3);

 const diffValueTypes = { foo: 123, bar: 'blah' };

 const updated = rebuild(([k, v]) =>[[k, v]], diffValueTypes);
-expectType<Record<string, string | number>>(updated);
+expectType<Record<'foo' | 'bar', string | number>>(updated);
diff --git a/types/mapKeys.d.ts b/types/mapKeys.d.ts
index f89dcb7..fe70498 100644
--- a/types/mapKeys.d.ts
+++ b/types/mapKeys.d.ts
@@ -5,4 +5,4 @@ export function mapKeys(fn: (key: string) => string): <T>(obj: Record<string, T>
 // mapKeys(__, obj)(fn)
 export function mapKeys<T>(__: Placeholder, obj: Record<string, T>): (fn: (key: string) => string) => Record<string, T>;
 // mapKeys(fn, obj)
-export function mapKeys<T>(fn: (key: string) => string, obj: Record<string, T>): Record<string, T>;
+export function mapKeys<T extends Record<string, unknown>, U extends string>(fn: (key: keyof T) => U, obj: T): Record<U, T[keyof T]>;
diff --git a/types/rebuild.d.ts b/types/rebuild.d.ts
index d8c92e3..44cca23 100644
--- a/types/rebuild.d.ts
+++ b/types/rebuild.d.ts
@@ -5,4 +5,4 @@ export function rebuild<T, V>(fn: (kvp: [keyof T, T[keyof T]]) => [string, V][])
 // rebuild(__, obj)(fn)
 export function rebuild<T>(__: Placeholder, obj: T): <V>(fn: (kvp: [keyof T, T[keyof T]]) => [string, V][]) => Record<string, V>;
 // rebuild(fn, obj)
-export function rebuild<T, V>(fn: (kvp: [keyof T, T[keyof T]]) => [string, V][], obj: T): Record<string, V>;
+export function rebuild<T, V, U extends string>(fn: (kvp: [keyof T, T[keyof T]]) => [U, V][], obj: T): Record<U, V>;

@RobinTail
Copy link
Contributor

RobinTail commented Jul 25, 2025

Similarly, I'd also like to propose this:

- export function mapKeys<T>(__: Placeholder, obj: Record<string, T>): (fn: (key: string) => string) => Record<string, T>;
+ export function mapKeys<T extends Record<string, unknown>>(__: Placeholder, obj: T): <U extends string>(fn: (key: keyof T) => U) => Record<U, T>;

and

- export function rebuild<T>(__: Placeholder, obj: T): <V>(fn: (kvp: [keyof T, T[keyof T]]) => [string, V][]) => Record<string, V>;
+ export function rebuild<T>(__: Placeholder, obj: T): <V, U extends string>(fn: (kvp: [keyof T, T[keyof T]]) => [U, V][]) => Record<U, V>;

It would add constraints to the postponed function, limiting the keys to the ones of the original subject, also improving the type of the keys in the resulting object.

@Harris-Miller

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

Successfully merging this pull request may close these issues.

2 participants