Home > Enterprise >  Possible to have generic constraints in Arrays?
Possible to have generic constraints in Arrays?

Time:03-01

Is it possible to have TypeScript constrain the 2nd index using the first argument?

For example:

type Data = {
  FOO: number
  BAR: string
}

type DataKeys = keyof Data

type SpecialArray<T extends DataKeys = DataKeys> = [T, Data[T]]

// I want both of these array to fail because the 2nd argument's type
// is determined by the first argument
const arr1: SpecialArray = ['FOO', 'wrong'] // 2nd argument should be `number`
const arr2: SpecialArray = ['BAR', 666] // 2nd argument should be `string`

// This works well with functions
const specialFunction = <T extends DataKeys = DataKeys>(key: T, value: Data[T]) => {
  return
}
specialFunction('FOO', 'wrong')
specialFunction('BAR', 666)

Link to playground

CodePudding user response:

Yes and no.

In this case:

type SpecialArray<T extends DataKeys = DataKeys> = [T, Data[T]]
const arr1: SpecialArray = ['FOO', 'wrong'] // no error

You omitted the generic parameter, so the default is used. And since the default type DataKeys is all keys then the resulting value type is number | string.

However, all generic parameters must have a type assigned to them, so it doesn't work if you only remove the default:

type SpecialArray<T extends DataKeys> = [T, Data[T]]
const arr1: SpecialArray = ['FOO', 'wrong'] // error
// Generic type 'SpecialArray' requires 1 type argument(s).(2314)

It does work if you specify the generic parameter:

// errors as expected
const arr1: SpecialArray<'FOO'> = ['FOO', 'wrong'] // 2nd argument should be `number`
const arr2: SpecialArray<'BAR'> = ['BAR', 666] // 2nd argument should be `string`

But that's not exactly pretty.

Sadly, you just can't infer a generic parameter from a simple assignment in typescript.

The typical workaround for this is to use a function to make the objects for you. Since, as you note functions handle this better.

function makeSpecialArray<
  T extends DataKeys
>(key: T, value: Data[T]): SpecialArray<T> {
  return [key, value]
}

// these are type errors, and the objects they create are SpecialArray<SomeKey>
const arr1 = makeSpecialArray('FOO', 'wrong') // 2nd argument should be `number`
const arr2 = makeSpecialArray('BAR', 666) // 2nd argument should be `string`

Playground

CodePudding user response:

In a situation like this you really just want SpecialArray to be a specific union type of the form ["FOO", number] | ["BAR", string]. This meets all your needs:

type SpecialArray = ["FOO", number] | ["BAR", string]
const arr1: SpecialArray = ['FOO', 'wrong'] // error
const arr2: SpecialArray = ['BAR', 666] // error
const arr3: SpecialArray = ['FOO', 123] // okay
const arr4: SpecialArray = ['BAR', 'right'] // okay

So the question here is just "how can I compute SpecialArray from Data?"


There's no reason to keep SpecialArray<T> generic in T, but if you do have it generic in T, you want to distribute the type operation across any unions in T. Your current version just produces a single tuple type:

type SpecialArray<T extends DataKeys = DataKeys> = [T, Data[T]]
type Test = SpecialArray;
// type Test = [keyof Data, string | number]

You can use a distributive conditional type to distribute type operations across unions:

type SpecialArray<T extends DataKeys = DataKeys> = 
   T extends any ? [T, Data[T]] : never;

type Test = SpecialArray; 
// type Test = ["FOO", number] | ["BAR", string]

The T extends any ? XXX : never looks like a no-op that produces XXX, but it has the desired effect of splitting DataKeys into "FOO" and "BAR", computing each tuple type separately, and then unioning them back together.


But as I said you don't need to have SpecialArray be generic; you can compute it from Data directly via something called a "distributive object type" (as coined in ms/TS#47109), where you make a mapped type over each key in DataKeys where the corresponding property is the tuple type you want, and then immediately index into it with DataKeys to get the union of all properties. Like this:

type SpecialArray = { [K in DataKeys]: [K, Data[K]] }[DataKeys];
// type SpecialArray = ["FOO", number] | ["BAR", string]

So that's probably what I'd suggest here; make SpecialArray a specific (that is, non-generic) distributive object type. And if Data is ever updated, SpecialArray will update automatically:

type Data = {
  FOO: number
  BAR: string
  BAZ: boolean
}

// type SpecialArray = ["FOO", number] | ["BAR", string] | ["BAZ", boolean];

Playground link to code

  • Related