typescripttypescript-genericscontravariance

What type can be used for a parameter of a generic method that will not be invoked?


I am trying to define a method with a parameter that has a generic type, using unknown as the generic type because I don't need it: function f(op: Operation<unknown>): void {...}. It does not work in every case, it does not work if Operation uses its generic type in a method signature.

If instead of a method having the generic Context in parameter I use directly a generic Context member, it compiles without errors.

Can someone explain why I can't use unknown if the generic is in the signature of a method?

I am trying to figure out why this sample does not compile:

export interface Operation<Context> {
    process: (context: Context) => void;
    //context: Context;
    n:number;
}

type MyContext = {
  info: string;
}

const op : Operation<MyContext> = {
  process: (context: MyContext) => { console.log("process",context.info); },
  //context: { info:"context.info" },
  n:42
}

function fGeneric<Context>(op: Operation<Context>): void {
     console.log("fGeneric", op.n);
}

console.log(fGeneric(op));

function fUnknown(op: Operation<unknown>): void {
     console.log("fUnknown", op.n);
}

console.log(fUnknown(op)); 
// Argument of type 'Operation<MyContext>' is not assignable to parameter of type 'Operation<unknown>'.
//  Type 'unknown' is not assignable to type 'MyContext'.

Commenting out process and uncommenting context compiles without error.

(Obviously this is a simplified example, boiled down to the minimum to exhibit the problem.)

playground : https://www.typescriptlang.org/play?ts=4.9.5#code/KYDwDg9gTgLgBASwHY2FAZgQwMbDgeTDUxgQiQB4Bhc1EGAPjgG8AoODuMKCXAZz4AuOAApstUDGE0UkgJRwAvEwBuEBABMA3O04B6PeNn1pE+js5wkgpAFcAtgCM0OgL6tWMAJ5E4AWS8ZOnhFFl1kdAhhPhgoZABzNw8jGLgIMDhhQmJScgoAoMkmULYObl5gAWExMyl-QNqFZRY4FIgAG2AAOnaIeJEAInL+PgGAGiNgroiIOS04VzHdA0nJYWZEJEjBAdX6aa2IAYWljmsAFgAmVndWdFskbFykOHQAcWAkNARsalqGETpLJEKAkMiUQr0BhyYRqTRhSytch8DrdXr9AbvT7fbDjNJgLpIOY3ZLI1E9PoiLFfOLYQFgOTEu4PJ7g14AVSQAGskBAAO5IenAnLgigPHn8pDQ2HqDQIyxtToUjHoTkSgV49KE4m3RVoymq7m8gX0xlaIA


Solution

  • Use the bottom type instead of the top type.

    unknown is the top type: it contains all possible values. A function taking an argument of type unknown should accept any value whatsoever as the argument. A function that only accepts values of some types cannot possibly conform to (_: unknown) => void; however, a function (_: unknown) => void will conform to (_: T) => void for any T. So we have that T is a subtype of unknown, but on the other hand (_: unknown) => void is a subtype of (_: T) => void. This situation is known as contravariance, and we say that the type constructor that takes T to a type of functions-that-take-a-T is contravariant in T.

    In your case, since the definition of Operation<T> specifies a property with a type of functions taking T, Operation<T> is contravariant in T as well. That means Operation<unknown> is a subtype of Operation<T> for any other T, not a supertype as you want. (When you changed Operation<T> to include a property of type just T, it instead became covariant in T, which means the subtype relationship is not reversed.)

    Instead, you can use Operation<never>:

    export interface Operation<Context> {
        process: (context: Context) => void;
        n: number;
    }
    
    type MyContext = {
        info: string;
    }
    
    const op : Operation<MyContext> = {
        process: (context: MyContext) =>
            { console.log("process", context.info); },
        n: 42
    }
    
    function fNever(op: Operation<never>): void {
        console.log("fNever", op.n);
    }
    
    console.log(fNever(op));
    

    The bottom type never contains no values and is a subtype of all types. Because Operation<T> is contravariant in T, this means that Operation<never> is a supertype of all Operation<T>. In plainer language, by taking an Operation<never>, you don’t require process to be callable with anything in particular, and in effect you promise you will never call process at all: because never contains no values, a function taking a never argument is uncallable.