I am trying to implement a simple version of match from Rust
It should work like this:
const value = Math.random() > 0.5 ? 1 : 'foo'
const result = match(value, {
1: () => 100,
foo: () => 'F'
})
result // 100 | 'F'
In object argument - keys must cover all the possible types of
value
(1
and'foo'
in example above) - otherwise TS should throw an error, that a particular property is missing.In object argument - values must be functions - otherwise TS should throw an error that value is not assignable to function
The "default case" is not important, I will try to implement it later
The best approach I have get:
export function match<
Case extends string | number,
Options extends Record<Case, any>
>(case: Case, options: Options): ReturnType<Options[Case]> {
return options[case]()
}
This workaround with any
keyword has one major problem - the code can break at runtime. When I try to type somehow this any
- I always end up with return type any
or unknown
.
(I know about conditional types and infer, but this knowledge doesn't help me in this case...)
How to type this match function?
Cheers!
CodePudding user response:
Try overload your function:
function match<
Key extends PropertyKey,
ReturnValue extends string | number,
Branch extends () => ReturnValue,
Options extends Record<Key, Branch>,
Variant extends keyof Options
>(options: Options, variant: Variant): ReturnType<Options[Variant]>
function match(options: Record<PropertyKey, () => unknown>,variant: PropertyKey, ) {
return options[variant]()
}
const result1 = match({
1: () => 100,
foo: () => 'F'
}, 1) // 100
const result2 = match({
1: () => 100,
foo: () => 'F'
}, 'foo') // snumber
I know, I have used unknown
, but I did not use it in my overload signature. If you hover your mouse on match
you will see that each object key/value is infered.
In order to infer literal type, you should either use literal object as a first argument or make it immutable.
I believe making it immutable is a way to go, since I assume it will be used in a several functions:
function match<
Key extends PropertyKey,
ReturnValue,
Branch extends () => ReturnValue,
Options extends Record<Key, Branch>,
Variant extends keyof Options
>(options: Options, variant: Variant): ReturnType<Options[Variant]>
function match(options: Record<PropertyKey, () => unknown>, variant: PropertyKey,) {
return options[variant]()
}
const strategy = {
1: () => 100 as 100,
foo: () => 'F' as 'F'
} as const
const result1 = match(strategy, 1) // 100
const result2 = match(strategy, 'foo') // F
If you want to learn more about function inference on arguments you can check my article