Home > Blockchain >  How to use generics to make argument types dependant on each other?
How to use generics to make argument types dependant on each other?

Time:01-05

The idea is that i can provide 2 arguments, whereas as the first argument would provide a list of strings, and for the second i only want to allow those values provided in the first argument to be the allowed keys of an object.

This is what i have tried.

declare function test<T extends readonly string[]>(a: T, b?: Record<typeof a[number], string>): unknown

test(['a', 'b'], {
  a: 'someValue',
  b: 'someOtherValue',
  c: 'thisShouldNotWork' // no error shown, as typescript only infers string[]
})

I know that you can create a UnionType from a tuple, so my thinking is this should somehow be possible. With Union to Tuple it would look something like this and it results in the expected behaviour:

const arg1 = ['a', 'b'] as const
type Arg1 = typeof arg1

declare function test<T>(a: T, b?: Record<T[number], string>): unknown

test<Arg1>(arg1, {
  a: 'someValue',
  b: 'someOtherValue',
  c: 'thisShouldNotWork'
})

What i would like is a solution somewhat like my initial approach where i do not need to extract the UnionTypes for all the arguments i need.

Any help is appreciated!

CodePudding user response:

Typescript will infer literal types if they are assigned to a type parameter that is constrained to a literal producing type (such as string)

The simplest solution would be to use a type parameter for the items in the array and constrain that to string:

declare function test<T extends string>(a: T[], b?: Record<T, string>): unknown

test(['a', 'b'], {
  a: 'someValue',
  b: 'someOtherValue',
  c: 'thisShouldNotWork' // no error shown, as typescript only infers string[]
})

Playground Link

CodePudding user response:

The primary issue here is that you want the compiler to infer string literal types like "someValue" | "someOtherValue" for the elements of the array you pass as an argument to test, but currently it is inferring string instead.

The compiler uses various heuristics to determine whether it should be inferring literal types or not. TypeScript 5.0 will very likely include the ability for you to add a const modifier to generic type parameter declarations to request the same sort of preference for literals as in a const assertion. This is implemented in microsoft/TypeScript#51865. Once this is released you can get your desired behavior as follows:

// TS5.0 
declare function test<const T extends readonly string[]>(
// -----------------> ^^^^^
  a: T, b?: Record<T[number], string>): unknown

test(['a', 'b'], {
  a: 'someValue',
  b: 'someOtherValue',
  c: 'thisShouldNotWork' // error
}

For now the easiest thing to do is to refactor so that your function is only generic in the element type of the array instead of the whole array. When you constrain a generic type parameter to string, it will tend to infer literal types for the type argument, as described in microsoft/TypeScript#10676:

declare function test<K extends string>(
  a: readonly K[], b?: Record<K, string>
): unknown

test(['a', 'b'], {
  a: 'someValue',
  b: 'someOtherValue',
  c: 'thisShouldNotWork' // error
});

This is probably the way I'd suggest you do it even after TS5.0 is released, unless you have some reason why you care about the specific shape of the input array.

Playground link to code

  • Related