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

79 Views Asked by At

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

3

There are 3 best solutions below

2
user3840170 On BEST ANSWER

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.

0
Apokralipsa On

You cannot pass an Operation<MyContext> to a function with a parameter of type Operation<unknown>, because that function could try to do an illegal modification of that parameter. As in:

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

You should probably do something like this instead:

function fUnknown2<T extends unknown>(op: Operation<T>): void {
    const foo: unknown = "Mystery";
    /* Would cause "Type 'unknown' is not assignable to type 'T'.
  'unknown' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'unknown'."
    op.context = foo; */
    console.log("fUnknown", op.n);
}
0
geoffrey On

The reason why it does not compile is that function parameters are contravariant and Operation uses the Context type parameter in a function parameter position.

When you pass op to fUnknown, op claims that it can only work with MyContext, but fUnknown requires that the Operation passed to it can work with anything, not only MyContext.

You can solve this in a number of ways:

  • require Operation<any>: if you don't use the generic information at all in the function body or in the rest of the signature, it's a totally valid option
  • require Operation<MyContext>: it's not very versatile
  • require Operation<T> and let T be inferred: when you pass op, T becomes MyContext, and you can use the information that T is generic in the function body/signature