I am working through a TypeScript playground and got most of the way there, except it is not autocompleting/typing the options
correctly:
type Definition = {
code: number
note: (opts: Record<string, unknown>) => string
}
const errors: Record<string, Definition> = {
invalid_form: {
code: 3,
note: ({ name }) => `Form '${name}' is not valid`,
},
invalid_type: {
code: 2,
note: ({ name, type }) => `Value '${name}' is not '${type}' type`,
},
missing_property: {
code: 1,
note: ({ name }) => `Property '${name}' missing`,
},
}
type Errors = typeof errors
type Name = keyof Errors
function findError<N extends Name>(name: N, options: Parameters<Errors[Name]['note']>[0]) {
const handler = errors[name]
const text = handler.note(options)
return new Error(text)
}
findError('invalid_type', { foo: 'string' })
Notice the foo: 'string'
at the bottom, it should be erroring, and when I type n
in the key, it should autocomplete name
since that is the defined property for invalid_type
(even though name property is an unknown type). It should also autocomplete invalid_type
as I type the string (which it is currently not, I'm guessing because of the Record<string, Definition>
type?).
How can I accomplish this in TypeScript? I would like to avoid having to create a "type" for each note
function options, that would be overkill since it is just a map of string keys to any stringable value. Because having to write a type for each note
function (of which there could be dozens / a hundred) would be overkill IMO, so hoping to avoid that if possible.
I feel like somehow the options: Parameter...
type must involve extends
and a conditional inferred type to get at the keys of the parameters, and build a type from that somehow, but I'm having trouble figuring out how that might work. I tried this, but it didn't change anything:
function findError<
N extends Name,
O extends Parameters<Errors[N]['note']>[0],
>(
name: N,
options: {
[P in keyof O]: O[P]
},
) {
const handler = errors[name]
const text = handler?.note(options)
return new Error(text)
}
findError('invalid_type', { foo: 'array' })
Or this:
function findError<N extends Name>(
name: N,
options: Parameters<Errors[N]['note']>[0] extends infer O
? {
[P in keyof O]: O[P]
}
: never,
) {
const handler = errors[name]
const text = handler?.note(options)
return new Error(text)
}
findError('invalid_type', { foo: 'array' })
Or even:
function findError<N extends Name>(
name: N,
options: Parameters<Errors[N]['note']>[0] extends infer O
? {
[KeyType in keyof O as {} extends Record<KeyType, unknown>
? never
: KeyType]: O[KeyType]
}
: never,
) {
const handler = errors[name]
const text = handler?.note(options)
return new Error(text)
}
findError('invalid_type', { foo: 'array' })
Note: I put the Record<string, Definition>
on the errors map so you get type hinting there too, when creating the definitions.
Update
I got the autocomplete to work:
function findError<N extends Name>(
name: N,
options: Parameters<Errors[N]['note']>[0],
) {
const handler = errors[name]
const text = handler?.note(options)
return new Error(text)
}
findError('invalid_form', { name: 'any', type: 'asdf' })
But the handler?.note(options)
is now giving me:
Argument of type '{ name: any; } | { name: any; type: any; } | { name: any; another: any; }' is not assignable to parameter of type '{ name: any; } & { name: any; type: any; } & { name: any; another: any; }'.
Type '{ name: any; }' is not assignable to type '{ name: any; } & { name: any; type: any; } & { name: any; another: any; }'.
Property 'type' is missing in type '{ name: any; }' but required in type '{ name: any; type: any; }'.ts(2345)