Search code examples
typescriptundefinednullabletypescript-genericssuper

Error with TypeScript Generics (Using `Partial<T>` for `? super T`)


TypeScript compilation fails due to an error related to generics that I can not explain. I have created a minimal example:

interface Processor<T> { process(data:T):void; }
class ArrayProcessor<T> implements Processor<Array<T>|null> { process(data:Array<T>|null):void {} }
  // intention (Java syntax, TS?): ... `implements Processor<List<? extends T>/*|null*/>` ...
const applyProcessor = <T,>(processor:Processor<Partial<T>>) => {};
  // intention (Java syntax, TS?): ... `<T> void applyProcessor(Processor<? super T> processor)` ...

applyProcessor<Array<number>>(new ArrayProcessor<number>());
//                      ERROR ~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The last line produces an error with this message:

Argument of type 'ArrayProcessor<number>' is not assignable to parameter of type 'Processor<(number | undefined)[]>'.
  Types of property 'process' are incompatible.
    Type '(data: number[] | null) => void' is not assignable to type '(data: (number | undefined)[]) => void'.
      Types of parameters 'data' and 'data' are incompatible.
        Type '(number | undefined)[]' is not assignable to type 'number[]'.
          Type 'number | undefined' is not assignable to type 'number'.
            Type 'undefined' is not assignable to type 'number'.(2345)

I tried to investigate what TypeScript considers assignable. The |null part might have an influence here:

const myArg1:Processor<Partial<Array<number>|null>> = new ArrayProcessor<number>(); // <- No error
const myArg2:Processor<Partial<Array<number>>> = new ArrayProcessor<number>(); // <- ERROR
// Type 'ArrayProcessor<number>' is not assignable to type 'Processor<(number | undefined)[]>'.(2322)

Why does the applyProcessor call fail? Where are these undefined in the error messages coming from? How could the first snippet be fixed?

Bonus question: Does TypeScript have an equivalent to Java's <? super T>? I would be happy to declare my intention ("... is processor / comparator / consumer / handler / ... of ...") without compilation errors even if the TypeScript compiler can not actually enforce the constraint.


Solution

  • The problem is in Partial.

    When you use Partial and explicit number generic argument new ArrayProcessor<number>() it causes an error because Partial applies undefined type. undefined is not the same as null.

    According to this type definition implements Processor<Array<T> | null you expect an array or null for Processor generic while Processor<Partial<T>> means that you expect smth different.

    In order to fix it, you can rewrite applyProcessor function:

    const applyProcessor = <T,>(processor: Processor<T | null>) => { };
    

    OR

    const applyProcessor = <T,>(processor: Processor<T>) => { };
    

    Full code:

    interface Processor<T> {
      process(data: T): void;
    }
    
    class ArrayProcessor<T> implements Processor<Array<T> | null> {
      process(data: Array<T> | null) { }
    }
    
    const applyProcessor = <T,>(processor: Processor<T|null>) => { };
    
    applyProcessor(new ArrayProcessor<number>());