Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make one generic type closely dependent on another in TypeScript?

I'm looking for a way to make my code more type-safe and would like to define a close relationship between some of my types when passing them to generic functions.

For example, for the given set of types:

interface FooParam {}
interface FooReturn {}

interface BarParam {}
interface BarReturn {}

and the following function:

function action<T, R>(foo: T): R

I'd like to closely bind FooParam with FooReturn, and BarParam with BarReturn, so the compiler allows the call only if a pair of associated types is passed as T and R and returns an error otherwise.

action<FooParam, FooReturn>(...) // bound types, OK
action<BarParam, BarReturn>(...) // bound types, OK

action<FooParam, BarReturn>(...) // types are not bound, ERROR
action<FooParam, string>(...) // types are not bound, ERROR

I've actually managed to achieve the above by defining two base interfaces, which will be later used as constraints on the generic types:

interface Param {}
interface Return<T extends Param>{
    _typeGuard?: keyof T
}

interface FooParam extends Param {}
interface FooReturn extends Return<FooParam> {}

interface BarParam extends Param {}
interface BarReturn extends Return<BarParam> {}

function action<T extends Param, R extends Return<T>>(foo: T): R

However this seems more like a workaround than a clean solution, especially given the _typeGuard?: keyof T field, which exists only so T actually matters and is checked, not ignored. Another downside is that every type must extend one of my custom interfaces, which sometimes is just not possible.

Is there a better and more universal way to get the described functionality?

like image 881
jsamol Avatar asked May 06 '20 15:05

jsamol


2 Answers

You can use a generic type and inference. If your requirements allow this, it could be easier than extending each type from base interface. This solution puts all your links in one place, so it has its own drawbacks, but it seems ok to me if you can't change original types:

type Return<T> =
  T extends BarParam ? BarReturn :
  T extends FooParam ? FooReturn :
  never;

You can see it in action Playground

like image 121
leonardfactory Avatar answered Nov 15 '22 23:11

leonardfactory


It sounds more like a function overload than generics.

interface FooParam {
    fooParam: string;
}
interface FooReturn {
    fooReturn: string;
}

interface BarParam {
    barParam: string;
}
interface BarReturn {
    barReturn: string;
}

function action(a: FooParam): FooReturn;
function action(a: BarParam): BarReturn;
function action(a: any): any {
  // implementation, but here you need to solve types first
  // but outside you can't call it like action<FooParam, BarReturn>
}

declare const foo: FooParam;
declare const bar: BarParam;


// you'll get
const fooReturn: FooReturn = action(foo); // bound types, OK
const barReturn: BarReturn = action(bar); // bound types, OK

const test1: BarReturn = action(foo) // types are not bound, ERROR
const test2: string = action(foo) // types are not bound, ERROR

I wouldn't suggest to use conditional types in generics here.

like image 26
satanTime Avatar answered Nov 15 '22 22:11

satanTime