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.)
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.