Resolving generics in TypeScript

In this article you will find some useful examples of typing factory functions which requires you to use several generics with constraints

#Context sensitive functions

Sometimes it is hard to get around with typescript generics. Especially when we want to apply some sort of type validation.

First thing that we should keep in mind, is that they are resolved from left to right.

Let's imagine a case where we need to implement a factory functions. Function should expect two arguments. First one methods expects a dictionary where key is a method name and value is a method which have an access to this. If user provides two key/value pairs as a first argument, this of each method should be aware about the other one.


factory({
  a: function () {
    this.id // string
    const b = this.b(); // Ok
    return "0";
  },
  b: function () {
    const b = this.b(); // Ok
    this.c() // expected error, [c] does not exists
    return 0;
  },
}, function () {
  this.id // string
  const a = this.a() // string
  const b = this.b(); // number
});

As you might have noticed, second argument method should be just a function which can access any of these methods. Also, there should be shared property id

Since we have all our requirements, we can try to type it. Because property id should be accessible from any argument, we can create base type.


type Base = { id: string }

Now, when we know that this of each method should implementBase type it is easy to type a prototype of afactory function


const factory = <
  T extends Base,
  Methods extends Record<string, (this: T) => unknown>,
  >(methods: Methods, method: (this: T) => void) => {
  return null as any
}

We still unable to access this.a and this.b


factory({
  a: function () {
    const b = this.b(); // error
    return "0";
  },
  b: function () {
    const b = this.b(); // error
    return 0;
  },
}, function () {
  const a = this.a() // error
  const b = this.b(); // error
});

Let's brainstorm Methods generic. Each methods thisshould be aware of each method. It means that this should be an intersection of Base type and Methods itself


const factory = <
  T extends Base,
  Methods extends Record<string, (this: T & Methods) => unknown>,
  >(methods: Methods, method: (this: T & Methods) => void) => {
  return null as any
}

factory(
  {
    a: function () { }, // error
  },
  function () { }
);

But, it does not work in a way we expect. The 'this' types of each signature are incompatible. This is not trivial.


factory(
  {
    a: function () { }, // is a context sensitive function
  },
  function () { }
);

Function a is context-sensitive. In order to better understand this type of problem, please seethis answer of A_blop user.

Here you can find related article

As you might have guessed, in order to fix it we need to provide extra generic Self


type Base = { id: string }

const factory = <
  T extends Base,
  Methods extends Record<string, <Self extends Methods>(this: T & Self) => unknown>,
  >(methods: Methods, method: (this: T & Methods) => void) => {
  return null as any
}

factory({
  a: function () {
    const b = this.b(); // Ok
    return "0";
  },
  b: function () {
    const b = this.b(); // Ok
    return 0;
  },
}, function () {
  const a = this.a() // string
  const b = this.b(); // number
});

If you are interested in runtime implementation of factoryfunction and other problems/issues you may face please seethisandthisanswers.

Btw,here you can find interesting use case. Code example from the question can be rewritten as follow:


interface BasicProps<T> {
  input: T;
}

interface TransformProps<T, O> {
  transform: (input: T) => O;
  render: <U extends O>(input: U) => any; // added extra generic U
}

type TProps<T, O> = BasicProps<T> & TransformProps<T, O>;

function test<T, O>(props: TProps<T, O>) {
  return props.render(props.transform(props.input));
}

const testData = {
  nested: {
    test: 1,
    best: true
  }
};

const testResult = test({
  input: testData,
  transform: input => input.nested,
  render: input => input.test // Error
});

The error still exists. TS i unable to figure out the type of render argument input. Once you rewrite render function to make it void - TS is able to infer input argument. This is strange. I'm unable to explain it.


const testResult = test({
  input: testData,
  transform: input => input.nested,
  render: input => {
    input.test // ok
  }
});

#Typing curried factories

Imagine that we have simple high order function that accepts another function and some object and returns another function:


const hof = (callback, data) => (model) => callback({ ...data, ...model });

The main requirements here is that we need exclude from model properties that already present in data.

Let's type it step by step. Our callback argument can be any one argument function, so we can use simple restriction:(arg: any) => void. The second argument datacan be a Partial of callback argument. I meanParameters<Callback>[0]. Let's implement it.


const factory = <
    Callback extends (arg: any) => void,
    Arg extends Parameters<Callback>[0],
    KnownProps extends Partial<Arg>,
    >(callback: Callback, knownProps: KnownProps) => (...[newProps]: any) =>
        callback({ ...knownProps, ...newProps });

Now we can type our second part.


interface GreeterData {
    greetings: string;
    userName: string;
}

const factory = <
    Callback extends (arg: any) => void,
    Arg extends Parameters<Callback>[0],
    KnownProps extends Partial<Arg>,
    >(callback: Callback, knownProps: KnownProps) =>
    (newProps: Omit<Arg, keyof KnownProps>) =>
        callback({ ...knownProps, ...newProps });

const greeter = (greeterData: GreeterData) => string;
const greet1 = factory(greeter, { greetings: "hello" });

greet1({ userName: 'a' })
greet1({ userName: 'a', greetings: 'a' }) // expected error

The argument of inner function meets our main requirement. However, this example has few extra requirements. If you are curious, you can find full codein the answer provided byDima Parzhitsky.

Btw, he is an author of famous answer about using project references in TypeScript 3.0

P.S. Here you can find an interesting example of typing Map constraints.

December 26, 2021