0

I have a problem with TypeScript generics in Deno.

abstract class Packet {
    public abstract read(): Uint8Array;
    public abstract write(data: Uint8Array): void;
}

type PacketData = Packet | Uint8Array;
type HookCallbackResult<T extends PacketData> = T | null | undefined;
type HookCallback<T extends PacketData> = (data: T) => HookCallbackResult<T>;

interface HookCallbackInfo<T extends PacketData> {
    packetType: typeof Packet | "any";
    callback: HookCallback<T>;
}

let callbacks: HookCallbackInfo<PacketData>[] = [];

function hook<T extends PacketData>(packetType: typeof Packet | "any", callback: HookCallback<T>) {
    callbacks.push({ packetType, callback });
    //                           ^^^^^^^^
    // error: TS2322 [ERROR]: Type 'HookCallback<T>' is not assignable to type 'HookCallback<PacketData>'.
    // Types of parameters 'data' and 'data' are incompatible.
    //     Type 'PacketData' is not assignable to type 'T'.
    //     'PacketData' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'PacketData'.
    //         Type 'Packet' is not assignable to type 'T'.
    //         'Packet' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'PacketData'.
}

My purpose is to force a callback which is passed as 2nd argument to hook function to return a result of the same type as its argument OR null OR undefined and this type should be Uint8Array or already parsed Packet (or its child class) and nothing else. And hook function should store this hook callback for further use. Deno's version of TypeScript compiler panics on adding a callback to an object (declared with interface HookCallbackInfo) array.

I guess I made a mistake somewhere with generics cause I don't understand properly how they work in TypeScript with these multiple type definitions (like Type1 | Type2 | Type3). Could you pls explain me what exactly I did wrong and how to do what I want in the proper way?

lem0nify
  • 684
  • 1
  • 6
  • 13

1 Answers1

1

Your callbacks array is an array of HookCallbackInfo<PacketData> values, which means their callback fields must contain functions of type (data: PacketData) => HookCallbackResult<PacketData>. In your hook function, you're passing in callbacks of some unknown type T that extends PacketData, so the callback you're trying to provide is a function of type (data: T) => HookCallbackResult<T>.

This is where the problem lies: callbacks needs functions that can be called on any PacketData value, whereas you're trying to provide it with callbacks that can only be called on some PacketData values. The type system detects and prevents you from doing so.

To see why this matters, consider what could go wrong if the type system didn't check this:

const myFn: (v: Packet) => null = ...
hook<Packet>("any", myFn);

const array: Uint8Array = ...
callbacks[0](array);  // we've called myFn with the wrong argument type!
jacobm
  • 13,790
  • 1
  • 25
  • 27
  • In fact, it will be Uint8Array if the 1st argument for hook function is "any". For every SpecificPacket type callback will be `HookCallback`. When these callbacks will be processed, I'm gonna check it and call "any"-callbacks for every incoming packet as RAW and if packet signature is implemented as a child of the Packet class (determined by opcode), call its specific callbacks too for its parsed version. So how should I implement this in proper way? – lem0nify Jul 30 '20 at 19:24
  • I ask you to explain me the right gerenic definitions for these data types considering my desired implementation, which won't panic, not implementation itself, of course. So could you please complete your answer? – lem0nify Jul 30 '20 at 19:28