Type alias RecordMapper<R, U, D>

RecordMapper<R, U, D>: {
    [K in keyof R]: ((value) => unknown extends U[K]
        ? D
        : U[K])
}

represents an Object consisting of a collection of single-parameter functions that map the entries of type R to entries of type U
however, if U does not contain a certain key that's in R, then we will assume that it is being mapped to a single default type D
to give you an idea, here is a flawed example: (more is covered on the flaw right after)

Type Parameters

  • R
  • U extends {
        [K in keyof R]?: any
    } = {
        [K in keyof R]: unknown
    }
  • D extends any = unknown

Example

declare RepeatedNamesDB: { name: string, repeatitions: number}
const my_stats_v1 = {name: "haxxor", game: "league of fools and falafel", fame: 505, tame: false, lame: ["yes", 735]}
const stats_v1_to_v2: RecordMapper<typeof my_stats_v1> = {
name: (s) => {
// `s` is automatically inferred as a `string`, thanks to `typeof my_stats_v1` generic parameter
let rep = RepeatedNamesDB[s]++
return [s, rep]
},
game: (s) => s,
fame: (v) => v * 1.5,
tame: (b) => undefined,
lame: (a) => ({
current_status: a[0] === "yes" ? true : false,
bad_reputation_history: [["pre-v2", a[1]], ["original-sin", 5], ]
})
}

uh oh, did you notice the problem? the IDE thinks that stats_v1_to_v2 maps each entry of my_stats_v1 to unknown.
you must provide a second type parameter that specifies the new type of each entry (which in this context would be stats_v2).

Example

// declare RepeatedNamesDB; const my_stats_v1 = {...};
type my_stats_v2 = {
name: [string, number],
game: string,
fame: number,
tame: undefined,
lame: {
current_status: boolean,
bad_reputation_history: Array<[occasion: string, value: number]>
}
}
const stats_v1_to_v2: RecordMapper<typeof my_stats_v1, my_stats_v2> = {
// just as before
}

but this is a lot of repetition in typing, and the additional type will be utterly useless if it's not being used elsewhere.
luckily, with the introduction of the satisfies operator in tsc 4.9, you can be far more consise:

Example

declare RepeatedNamesDB: { name: string, repeatitions: number}
const my_stats_v1 = {name: "haxxor", game: "league of fools and falafel", fame: 505, tame: false, lame: ["yes", 735]}
// the map function parameters `s`, `v`, `b`, and `a` all have their types automatically inferred thanks to the `satisfies` operator
// `stats_v1_to_v2` now indeed maps the correct `stats_v2` interface
const stats_v1_to_v2 = {
name: (s) => {
let rep = RepeatedNamesDB[s]++
return [s, rep]
},
game: (s) => s,
fame: (v) => v * 1.5,
tame: (b) => undefined,
lame: (a) => ({
current_status: a[0] === "yes" ? true : false,
bad_reputation_history: [["pre-v2", a[1]], ["original-sin", 5], ]
})
} satisfies RecordMapper<typeof my_stats_v1>

now, for an example that uses the D default type generic parameter (3rd parameter):

Example

const now_i_know_my = { a: 1, b: 2, c: 3, s: "nein" }
const latin_to_greek: RecordMapper<
typeof now_i_know_my, // these are the inputs that will be mapped
{ s: number }, // entry `"s"` will be mapped to a `number`
string // all other entries will be mapped to `string`
> = {
a: (v) => `${v}-alpha`,
b: (v) => `${v}-beta`,
c: (v) => `${v}-theta`,
s: (v) => 9
}

Generated using TypeDoc