1

I have a Vue project in Typescript, and I'm running into an issue regarding the mapping of an Object of Tuples to an Object of Union types.

For a bit of context, I'm working on a Backend endpoint's expected response types. Currently, this endpoint receives 2 values: an enum, and a string. Depending on the enum the response object will change.

This is the current implementation:

const validations = {
  email: ['isValid', 'isAvaliable'],
  password: ['isMinLengthValid', 'hasUppercase', 'hasLowercase', 'hasSpecialChars', 'hasNumbers'],
  iban: ['isValid', 'isRegistered']
} as const

type ValidationsMap = {
  [T in keyof typeof validations]: typeof validations[T][number]
}

function validate<T extends keyof ValidationsMap>(type: T, value: string): Record<ValidationsMap[T], boolean> {
  // Do something
}

Now the Backend endpoint will receive one more parameter, and the response object will depend on it too.

This is what I've tried, but it's not working:

const validations = {
  portal: {
    email: ['isValid', 'isAvaliable'],
    password: ['isMinLengthValid', 'hasUppercase', 'hasLowercase', 'hasSpecialChars', 'hasNumbers'],
  },
  payment: {
    email: ['isValid'],
    iban: ['isValid', 'isRegistered']
  }
} as const

type ValidationsMap = {
  [S in keyof typeof validations]: {
    [T in keyof typeof validations[S]]: typeof validations[S][T][number] // Error: Type 'number' cannot be used to index type...
  }
}

function validate<S extends keyof ValidationsMap, T extends keyof ValidationsMap[S]>(service: S, type: T, value: string): Record<ValidationsMap[S][T], boolean> {
  // Do something
}

Does anyone know why this doesn’t work?

I thought there might be a limit to how deep it will allow to map a Tuple to a Union, but it doesn't seem to be the case.

This works correctly:

type PortalEmailValidation = typeof validations['portal']['email'][number]
1
  • 1
    I'd say this is due to a TS bug, ms/TS#27709, which doesn't seem like it will be fixed anytime soon. Workarounds involve a sort of redundant constraint, like this approach shows. Does that meet your needs? If so I could write up an answer; if not, what am I missing? Commented Jun 1, 2022 at 15:35

1 Answer 1

1

This seems to be a bug in TypeScript, as described in microsoft/TypeScript#27709. The type checker apparently doesn't properly track the constraints for deep index access types when the keys are generic. That issue has been open for a long time with no sign of progress, so for now all we can do is work around it.


One approach when the compiler won't accept an index access of the form T[K] is to use conditional type inference, like T extends Record<K, infer V> ? V : never (using the Record<K, V> utility type). If K is a (non-optional) key of T, then T will be seen as a Record<K, V> for some V, which we infer.

So instead of typeof validations[S][T][number], we can write typeof validations[S][T] extends Record<number, infer V> ? V : never. Or equivalently:

type ValidationsMap = {
  [S in keyof typeof validations]: {
    [T in keyof typeof validations[S]]:
    typeof validations[S][T] extends { [k: number]: infer V } ? V : never
  }
}

Another approach is to explicitly add back in the missing constraint. If you have a type A that you know is assignable to another type B but the compiler doesn't know this, then you can replace A with Extract<A, B> (using the Extract<T, U> utility type) and the compiler will accept it. It knows Extract<A, B> is assignable to B. And assuming you're right about A being assignable to B, then Extract<A, B> will evaluate to just A.

So if ValidationsMap[S][T] is assignable to string but the compiler can't see it, we can write Extract<ValidationsMap[S][T], string> instead:

function validate<S extends keyof ValidationsMap, T extends keyof ValidationsMap[S]>(
  service: S, type: T, value: string
): Record<Extract<ValidationsMap[S][T], string>, boolean> { // okay
  return null!
}

Playground link to code

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.