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:
baris still of typeAbcfoobaris holding on toAbc'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!