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)
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`
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];