I store data in objects with different versions that use different keys and different types.
The acceptable types for the data properties are defined in an interface that defines the versions as a dictionary type.
I want to match the object with handler functions based on a matching key in the data.
I create an dictionary value of handlers that stores the functions indexed by the associated key.
I want to make a generic ‘handle’ function that takes one of the data objects, a dictionary of all handlers, callback that receives a matching value and handler function.
In the callback I want to execute the handler with the matching value along with some external data available in the scope.
I cannot get the callback that I pass to my handle function to receive a matching pair of value and handler, even by sharing the same generic key.
The type of the value passed to the callback is a union of all the possible versions, not the single matching version.
How can I avoid define the types to avoid the type error and type the value and handler passed to the callback as a version match?
export interface VersionDictionary {
alpha: string
beta: number
}
export type VersionKey = keyof VersionDictionary
export type Handler<Key extends VersionKey, OtherProps, Result> = (props: OtherProps & {
value: VersionDictionary[Key]
}) => Result
export interface Matched <Key extends VersionKey, OtherProps, Result> {
handler: Handler<Key, OtherProps, Result>
value: VersionDictionary[Key]
}
function stringHandler (props: {
value: string
label: string
}): string {
return `${props.label}: ${props.value}`
}
function numberHandler (props: {
value: number
label: string
}): string {
return `${props.label}: ${props.value}`
}
// Switching the order of the handlers object keys will correctly cause an error when passed to the handle function
// because the type of props.value would not match the versions dictionary
const handlers = {
alpha: stringHandler,
beta: numberHandler
}
const alphaData = {
alpha: 'hello'
}
const betaData = {
beta: 42
}
function match<OtherProps, Key extends VersionKey, Result> (
data: Partial<VersionDictionary>,
handlers: {
[Key in VersionKey]: Handler<Key, OtherProps, Result>
},
key: Key
): Matched<Key, OtherProps, Result> | undefined {
const value = data[key]
if (value == null) {
return undefined
}
const handler = handlers[key]
return { handler, value }
}
function handle <OtherProps, Result> (
dictionary: Partial<VersionDictionary>,
handlers: {
[Key in VersionKey]: Handler<Key, OtherProps, Result>
},
execute: <Key extends VersionKey> (props: {
value: VersionDictionary[Key]
handler: Handler<Key, OtherProps, Result>
}) => Result
): Result {
let key: VersionKey
for (key in handlers) {
const matched = match(dictionary, handlers, key)
if (matched == null) {
continue
}
return execute({ value: matched.value, handler: matched.handler })
}
throw new Error('Unknown key')
}
const label = `Test (${new Date().toISOString()})`
// I want to type these callbacks as guaranteed to receive a matching type for the callback's value prop
// and the handler's value prop
const alphaResult = handle(
alphaData,
handlers, // This argument correctly throws an error if the prop types do not match the keys in the handlers object, which is an important feature I need to maintain
(props) => {
// Error: Type 'number' is not assignable to type 'string'.
// I want this to know that the type of props.value will match the type of the value prop in the handler
return props.handler({ value: props.value, label })
}
)
console.log(alphaResult)
const betaResult = handle(betaData, handlers, (props) => {
// Error: Type 'number' is not assignable to type 'string'
// Again, I want this to know that the type of props.value will match the type of the value prop in the handler
return props.handler({ value: props.value, label })
})
console.log(betaResult)