2
\$\begingroup\$

I implemented as an exercise a function which maps over object values in Typescript, and I am truly horrified by my type annotations.

The function:

type map = <F, T>(f:callback<F, T>) => mapping<F, T>;
type callback<F, T> = (value:F, index:number, entries:Entry<F>[]) => T;
type Entry<T=any> = [key:String, value:T];
type mapping<F, T> = <I extends Dic<F>>(obj:I) => Protect<I, Record<keyof I, T>>;
type Dic<T> = Record<string, T>;
type Protect<A, B> = A extends B ? A : B;
const map:map = f => obj =>
 Object.fromEntries(
 Object.entries(obj)
 .map(([key, value], i, arr):Entry =>
 [key, f(value, i, arr)]));

Example of usage:

const double = map((x:number) => x * 2);
const shout = map((x:any) => String(x) + '!');
type Abc = {
 a: number,
 b: number,
 c: number,
};
const foo:Abc = {a: 1, b: 2, c: 3}; // Abc
const bar = double(foo); // Abc {a: 2, b: 4, c: 6}
const foobar = shout(foo); // Record<keyof Abc, string> {a: '1!', b: '2!', c: '3!'}

I wanted to preserve as much type information as possible:

  • bar is still of type Abc
  • foobar is holding on to Abc's keys.

I also get type checking when writing the callback.

// @ts-expect-error
const typeError = map((x:number) => x)({a: 1, b: "2"})

I don't know if I over-engineered it but I can't stand to look at this code and this often happens to me in Typescript. I end up extracting as many things as possible in type aliases and I'm not sure it's such a good idea. it clutters the namespace, I have to find good names, type aliases don't expand in VSCode tooltips which is sometimes annoying and I still find the result difficult to look at.

  • Could this be simplified?
  • Just how do you manage long or complex type definitions so that your code looks good?

Thank you!

asked Apr 16, 2021 at 18:33
\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

It would be much easier for TS to infer the types if you put your callback in second place.

I mean const map = obj => f => {}

Also I slightly refactored your function in order to reduce complexity:

type Primitives = string | number;
const apply = <
 Key extends string,
 Value,
 >(obj: Record<Key, Value>) =>
 <Result,>(cb: (value: Value) => Result) =>
 (Object.keys(obj) as Array<Key>)
 .reduce((acc, elem) => ({
 ...acc,
 [elem]: cb(obj[elem])
 }), {} as Record<Key, Result>)
type Abc = {
 a: number,
 b: number,
 c: number,
};
const foo = { a: 1, b: 2, c: 3 }; // Abc
const map = apply(foo)
const double = (x: number) => x * 2
const shout = <T extends { toString: () => string }>(x: T) => `${x.toString()}!`
const promisify=<T,>(arg:T)=>Promise.resolve(arg)
const bar = map(double) // Record<"a" | "b" | "c", number>
const foobar = map(shout); // Record<"a" | "b" | "c", string>
const baz = map(promisify) // Record<"a" | "b" | "c", Promise<number>>
Toby Speight
87.2k14 gold badges104 silver badges322 bronze badges
answered Jun 1, 2021 at 7:11
\$\endgroup\$
2
  • \$\begingroup\$ Well, you have change both the behaviour of the function and the behaviour of the types, so I would not consider that a refactor. There is a discussion to be had about the types: do you find overzealous to preserve the name of the type as I did? Concerning the order of the arguments, it's important that data comes last if you want to partially apply the behaviour. It is a generally more useful pattern in functional programming. It's the data that flows. \$\endgroup\$ Commented Jun 9, 2021 at 13:31
  • \$\begingroup\$ Ok, I will consider it next time \$\endgroup\$ Commented Jun 9, 2021 at 16:00
0
\$\begingroup\$

I gave it another go, which achieves the same functionality

type Transform<From, To> = (
 value: From,
 index: number,
 entries: [unknown, From][]
) => To;
type Choose<A, B> = A extends B ? A : B;
const map = <From, To>(t: Transform<From, To>) => 
 <Input extends Record<string, From>>(obj: Input) => 
 Object.fromEntries(
 Object.entries(obj).map(
 ([key, val], i, arr) => [key, t(val, i, arr)])
 ) as Choose<Input, Record<keyof Input, To>>;

I don't like the indentation, I don't like to have types in the function body, but I have to admit it's a lot better than my previous attempt which was creating too many abstractions, and also bad ones (mapping isn't so natural)

Working with Object.entries/fromEntries is also problematic because the keys are always going to be of type string when I wanted keyof Iput, hence the as which I also don't like.

answered Jun 9, 2021 at 18:18
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.