Home > Software engineering >  Simple rust "match" implementation in TypeScript. How to type the function?
Simple rust "match" implementation in TypeScript. How to type the function?

Time:11-15

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'
  1. 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.

  2. In object argument - values must be functions - otherwise TS should throw an error that value is not assignable to function

  3. 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


Playground

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

Playground

If you want to learn more about function inference on arguments you can check my article

  • Related