I'm having trouble with TypeScript not throwing type errors in my code. I have a function registerIpcChannel
that registers an IPC channel with a specific handler. The handler should match the types defined in the IpcChannel
interface. This works for the most part, however, when I intentionally return undefined
or null
from the handler, TypeScript does not throw a type error as expected. Here's a simplified version of my code:
/**
* Interface that describes an IPC channel
* @template P - Type of the parameters for the handler
* @template R - Type of the return value for the handler
*/
interface IpcChannel<P extends any[], R> {
name: string;
}
/**
* Function that takes in a IpcChannel interface, handler function, registers it
*/
const registerIpcChannel = <P extends any[], R>(
ipcChannel: IpcChannel<P, R>,
handler: (...params: P) => R
): void => {
// Do things...
};
/**
* Create definition of an IPC channel
* Accepts a single parameter that is a string, returns a string
*/
const testIpcChannel: IpcChannel<[string], string> = {
name: '/foo/bar',
};
//
// Testing the register function
//
/**
* Register the IPC channel
* Sanity check passes, no type error is thrown.
*/
registerIpcChannel(testIpcChannel, (arg) => {
return 'This returns a string: ' + arg;
});
/**
* Test with returning the wrong type
* Sanity check passes, a type error is thrown because a number is returned.
*/
registerIpcChannel(testIpcChannel, (arg) => {
return 1;
});
/**
* Test with returning undefined
* Sanity check FAILS, this should throw a type error
*/
registerIpcChannel(testIpcChannel, (arg) => {
return undefined;
});
The last registerIpcChannel
call that returns undefined
should be throwing a type error, however it is not.
I found that if I constrain the return type of handler
to NonNullable
, that will make a type error be thrown. However that then breaks situations where the handler function returns void
.
const registerIpcChannel = <P extends any[], R>(
ipcChannel: IpcChannel<P, R>,
handler: (...params: P) => NonNullable<R> // + Added `NonNullable`
): void => {
// Do things...
};
/**
* Void test
* Fails, this now throws an error after adding `NonNullable`
*/
const voidIpcChannel: IpcChannel<[string], void> = {
name: '/foo/bar',
};
registerIpcChannel(voidIpcChannel, (arg) => {
return;
});
Interestingly, it seems that if I refactor the way things are structured by moving the handler definition to the IpcChannel
interface then things behave how I would expect.
//
// Handler on the `IpcChannel` interface
//
interface IpcChannel2<P extends any[], R> {
name: string;
handler: (...args: P) => R;
}
/**
* Sanity check passes, a type error is thrown.
*/
const test: IpcChannel2<[string], string> = {
name: '/foo/bar',
handler: (arg) => {
return undefined;
},
};
However, due to limitations I cannot do this. I need to have the handler function defined separately from the IpcChannel
interface.
The problem you're having seems to be that if you define registerIpcChannel
like
const registerIpcChannel = <P extends any[], R>(
ipcChannel: IpcChannel<P, R>,
handler: (...params: P) => R
): void => {
// Do things...
};
with registerIpcChannel(channel, handler)
, TypeScript will infer R
from handler
as well as from ipcChannel
. So if ipcChannel
is IpcChannel<P, X>
and handler
is (...params: P)=>undefined
then TypeScript infers R
as X | undefined
.
Since your IpcChannel<P, R>
is structurally independent of P
and R
(this is a problem you should fix but it's out of scope for the question as asked) then IpcChannel<P, X | undefined>
matches IpcChannel<P, X>
and you get no error when you want one.
It seems therefore that you really don't want to allow TypeScript to use handler
to infer R
, and instead want to use ipcChannel
alone (keeping in mind that this doesn't always work). If so, you could use the NoInfer<T>
utility type to block inference there:
const registerIpcChannel = <P extends any[], R>(
ipcChannel: IpcChannel<P, R>,
handler: (...params: P) => NoInfer<R>
): void => {
// Do things...
};
And now your tests behave as expected:
const testIpcChannel: IpcChannel<[string], string> = {
name: '/foo/bar',
};
registerIpcChannel(testIpcChannel, (arg) => {
return 'This returns a string: ' + arg; // okay
});
registerIpcChannel(testIpcChannel, (arg) => 1); // err
registerIpcChannel(testIpcChannel, (arg) => undefined); // err
const voidIpcChannel: IpcChannel<[string], void> = {
name: '/foo/bar',
};
registerIpcChannel(voidIpcChannel, (arg) => {
return;
}); // okay