typescripttypeslocalutilityincompatibility

Defining Local Types Without Incompatibility Errors in TypeScript


TypeScript Version: 3.8.2

Search Terms: map, over, utility, type, result, keys, extract

Code

The following helper is working for me beautifully:

export type ComputeUnionValue<V extends AnyCodec[]> = {
  [i in Extract<keyof V, number>]: GetValue<V[i]>;
}[Extract<keyof V, number>];

I try to simplify this helper, which uses Extract<keyof V, number> in two places.

export type ComputeUnionValue<V extends AnyCodec[], E = Extract<keyof V, number>> = {
  [i in E]: GetValue<V[i]>;
}[E];

The E of [i in E] errors with:

Type 'E' is not assignable to type 'string | number | symbol'.
  Type 'E' is not assignable to type 'symbol'.ts(2322)

And the V[i] errors with:

Type 'V[i]' does not satisfy the constraint 'AnyCodec'.
  Type 'V[E]' is not assignable to type 'Codec<VType, unknown>'.
    Type 'AnyCodec[][E]' is not assignable to type 'Codec<VType, unknown>'.ts(2344)

and

Type 'i' cannot be used to index type 'V'

I'm using E = for the sake of having that new type within scope... not as an optional argument. But the fact that it's an optional argument seems to affect the "guarantees" so to speak, which results in the incompatibilities above. Is there a way to create the E type––local to this utility type––in such a way that there's no potential for type incompatibility?


Solution

  • While I have seen type parameters used as 'local types variable', I don't think this is a good idea because they basically expose internal logic to the outside (after all someone could pass in E instead of using the default). They also mean the user has to know which parameters are "real" and which are just "local" which is bad DX in my opinion. (FYI: there was a proposal on GH to allow local types aliases but I don't think it has gone anywhere)

    That being said, the reason you are getting an error is that = just provides a default for a type parameter, but the type parameter could be ANY type, including a type that can't be used in a mapped type.

    The solution is simple, if a bit verbose, provide a default and a constraint (using extends) for the type parameter:

    export type ComputeUnionValue<V extends AnyCodec[], E extends Extract<keyof V, number> = Extract<keyof V, number>> = {
      [i in E]: GetValue<V[i]>;
    }[E];
    

    Playground Link