Infer function generic type U from return value of passed function

Viewed 527

A function with a generic type may infer the generic types from the arguments in most cases. However, if an argument is a function where the generic type is both part of the arguments and the return value, the generic type is sometimes not inferred.

Dumbed down example with a class for storing items, and where some attributes of items are auto-generated (e.g. a database where IDs are auto-generated):

/**
 * Stores items of type T
 */
class Store<T> {

  /**
   * Return a function that creates items from supplied partial items merged with
   * attributes U auto-generated by a generator function
   */
  itemCreator<U>(
    generate: (item: Omit<T, keyof U>) => U
  ): (item: Omit<T, keyof U>) => Omit<T, keyof U> & U {
    return item => ({...item, ...generate(item)});
  }
}

type Person = {
  id: string;
  name: string;
  email: string;
  age?: number;
};

So, if you create a Store<Person> and supply a generator to auto-generate id, the returned creator function would only require name and email.

However, in some cases, U is not inferred:

Works:

const create = new Store<Person>()
  .itemCreator(() => ({id: 'ID', extra: 42}));
// U is {id: string, extra: number}, `create` only needs to provide `name` and `email` :)
const person = create({name: 'John', email: 'john.doe@foo.com'}); // creates person with extra

Does not work:

const create = new Store<Person>()
  .itemCreator(item => ({id: 'ID', extra: 42}));
// U is now unknown, meaning the `create` function must provide complete `Person` objects :(
const person = create({name: 'John', email: 'john.doe@foo.com'}); // does not compile

Works with explicit <U>:

const create = new Store<Person>()
  .itemCreator<{id: string, extra: number}>((item) => ({id: 'ID', extra: 42}));
const person = create({name: 'John', email: 'john.doe@foo.com'}); // creates person with extra

Now, the reason for passing the partial item to the generator is that some auto-generated attributes might be dependent on other attributes (such as an id being generated as a hash of an email attribute):

const creator = new Store<Person>()
  .itemCreator(item => ({id: hash(item.email)}))

So, my question is why inferring U fails if the argument to the generate function is provided? Does TypeScript simply use the first found instance of U or what is the reason? The generate function returns U, so if it sees {id: string} being returned, one could argue that the fact that U is also present in the Omit type of the argument to generate should be irrelevant?

Is there a way to solve this?

1 Answers

@JHH, @Linda Paiste what do you think about next solution ?:


class Store<T> {

    itemCreator<U>(
        generate: <P = Omit<T, keyof U>>(item: P) => U
    ): (item: Omit<T, keyof U>) => Omit<T, keyof U> & U {
        return item => ({ ...item, ...generate(item) });
    }
}

type Person = {
    id: string;
    name: string;
    email: string;
    age?: number;
};


const create = new Store<Person>()
    .itemCreator(item => {
        const x = item // Omit<Person, "id" | "extra">
        return ({ id: 'ID', extra: 42 })
    });
const person = create({ name: 'John', email: 'john.doe@foo.com' });


It is looks like it is hard for TS to infer item argument, so I defined extra generic with default value.

Here, in my blog, you can find more interesting callback typings

Here you can find explanation of this behaviour by Titian Cernicova Dragomir

If there is a parameter, the checker needs to decide U before checking the body, since there is no source for inference it goes with unknown. It then checks the body, but does not go back to try again with U = ret type it just checks the return compa. with U = unknown

Related