I often use mapping objects as a more readable and salable alternative to switch, e.g. like this:
interface Foo {
type: 'foo';
foo: number;
}
interface Bar {
type: 'bar';
bar: string;
}
type FooBar = Foo | Bar;
const mapper: Record<FooBar['type'], () => string | number> = {
foo: () => 1,
bar: () => 'bar',
}
const foo = { type: 'foo', foo: 1 } satisfies Foo;
const bar = { type: 'bar', bar: '123' } satisfies Bar;
mapper[foo.type]() // -> 1
mapper[bar.type]() // -> 'bar'
This Record-based approach is fine in most places, but struggles when the object that is being mapped needs to be passed to the function:
// same Foo, Bar and FooBar types as before
const mapper: Record<FooBar['type'], (fooBar: FooBar) => string | number> = {
// the function's parameters could and should be typed as Foo and Bar respectively
// instead of FooBar in both cases
foo: (fooBar) => (fooBar as Foo).foo,
bar: (fooBar) => (fooBar as Bar).bar,
}
const foo = { type: 'foo', foo: 1 } satisfies Foo;
const bar = { type: 'bar', bar: '123' } satisfies Bar;
mapper[foo.type](foo) // -> 1
mapper[bar.type](bar) // -> 'bar'
// also works
[foo, bar].forEach(fooOrBar => {
mapper[bar.type](fooOrBar)
})
This is possible using key remappings, like this. However, the forEach() breaks now:
// same Foo, Bar and FooBar types as before
type Mapper = {
[E in FooBar as E['type']]: (obj: E) => number | string;
};
const mapper: Mapper = {
foo: foo => foo.foo,
bar: bar => bar.bar,
};
const foo = { type: 'foo', foo: 1 } satisfies Foo;
const bar = { type: 'bar', bar: 'bar' } satisfies Bar;
mapper[foo.type](foo); // -> 1
mapper[bar.type](bar); // -> 'bar'
// does not work any more, Foo & Bar will be reduced to never
[foo, bar].forEach(fooOrBar => {
mapper[bar.type](fooOrBar)
})
Is it possible to make the forEach() from the last example work, while not giving up on the improved typing provided by the key remapping?