Search code examples
typescripttypescript-typingstypescript-genericsreact-typescript

How to Restrict a Union Literal Type to One of the Literal Type


I want to create a type predicate so that when I use filter on array of Union of generics, it would return the correct type.

I wrote a type to indicate the input/output type of the convert function for the corresponding id:

type ConverterGroupIdToFunc = {
  text: [string, string]
  image: [File, string]
}

type ConverterGroupId = keyof ConverterGroupIdToFunc

type ConverterGroup<T extends ConverterGroupId> = {
  id: T
  convertFunc: (s: ConverterGroupIdToFunc[T][0]) => (ConverterGroupIdToFunc[T][1]);
}

And I also need a ConverterGroup array to store 2 different types of generics (ConverterGroup<"text">|ConverterGroup<"image">)[]

type ConverterGroupUnion = ConverterGroup<"text"> | ConverterGroup<"image">

For converterGroups: (ConverterGroup<"text">|ConverterGroup<"image">)[], I'd like find() to return type ConverterGroup<"text"> if the converterGroup's id is also "text".

I am trying to use a type predicate to tell find() to return the correct type but couldn't find how:

function isConverterGroup<T extends ConverterGroupId>(converterGroupId: T) {
  return (converterGroup: ConverterGroupUnion): converterGroup is ConverterGroup<T> => {
    return converterGroup.id === converterGroupId;
  }
}

const convertFunc = converterGroups.find(isConverterGroup("text"))!.convertFunc;

The above code gives me the error:

A type predicate's type must be assignable to its parameter's type.
  Type 'ConverterGroup<T>' is not assignable to type 'ConverterGroupUnion'.
    Type 'ConverterGroup<T>' is not assignable to type 'ConverterGroup<"text">'.
      Types of property 'id' are incompatible.
        Type 'T' is not assignable to type '"text"'.ts(2677)

Here is the Typescript Playground Link


Solution

  • Sometimes you know that a type X is assignable to a type Y, but TypeScript can't see it because either X or Y depend on some generic type. In your example, ConverterGroup<T> is generic and the compiler can't figure out that it's assignable to ConverterGroupUnion. And since a custom type guard function requires that for y is X the type X is assignable to typeof y, it complains with converterGroup is ConverterGroup<T>.

    In situations like this, you can usually fix it by changing X to either X & Y or Extract<X, Y> or Extract<Y, X> depending on the use case. An intersection is always assignable to its members, and so is the result of the union-filtering Extract utility type. If you're right that X is assignable to Y, then eventually X & Y and Extract<X, Y> (and maybe Extract<Y, X>, depending on the form of Y and X) will be just X once the generic is specified, and the resulting behavior doesn't change. If you're wrong about the assignability then you'll end up with something else, possibly never, so you should be careful that you know what you're doing with the types.

    Anyway since in your case it looks like you just want to filter the ConverterGroupUnion to the member that is assignable to ConverterGroup<T>, you can use Extract<ConverterGroupUnion, ConverterGroup<T>>:

    function isConverterGroup<T extends ConverterGroupId>(converterGroupId: T) {
      return (converterGroup: ConverterGroupUnion):
        converterGroup is Extract<ConverterGroupUnion, ConverterGroup<T>> => {
        return converterGroup.id === converterGroupId;
      }
    }
    

    Now there's no error, and your other code works as expected:

    const convertFunc = converterGroups.find(isConverterGroup("text"))!.convertFunc;
    // const convertFunc: (s: string) => string
    

    Note that if you did something weird like

    const x = isConverterGroup(Math.random() < 0.5 ? "text" : "image");
    // const x: (converterGroup: ConverterGroupUnion) => converterGroup is never
    

    You get that never I was mentioning, because ConverterGroup<"text" | "image"> is not related to either ConverterGroup<"text"> or ConverterGroup<"image">, since ConverterGroup<T> is invariant in T (see Difference between Variance, Covariance, Contravariance, Bivariance and Invariance in TypeScript). Which is part of the reason for the error in the first place.

    Playground link to code