subvertallchris subvertallchris - 3 years ago 212
TypeScript Question

TypeScript generics and function arguments

I'm working on improving the defs for Rosie, which currently rely a lot on

any
and don't offer much in the way of safety. I'm a little stuck at the moment and could use some advice.

I'm trying to write a signature that expresses the following:

// this is invalid, but something like this is the goal
interface IFactory<T = any> {
attr<K extends keyof T, D extends keyof T>(name: K, dependencies: D[], generatorFunction: (value1?: D[0], value2?: D[1], value3?: D[2]), value4?: D[3] => T[K]): IFactory<T>;
}


An array of keys is given in the second argument. The values of those are passed in as arguments to the function in the same order provided. I want to avoid unnecessary type casts, so we should get this:

Factory.define<Person>('Person').attr('fullName', ['firstName', 'lastName', 'age'], (firstName, lastName, age) => {
// it knows that firstName is a string, lastName is a string, age is a number

if (age > 10) {
// this will error
return age;
}

return `${firstName} ${lastName};
});


The closest I can get is this:

attr<K extends keyof T, D extends keyof T>(name: K, dependencies: D[], generatorFunction: (value1: T[D], value2: T[D], value3: T[D], value4: T[D]) => T[K]): IFactory<T>;


That will type up to 4 dependent values, but calling it requires explicit casts and it doesn't set types in the correct order:

// it knows that each of the three arguments are string | number
existingDefinition.attr('fullName', ['firstName', 'lastName', 'age'], (firstName: string, lastName: string, age: number) => `${firstName} ${lastName}`);


This makes it possible for me to change the order of dependencies without it breaking, which is no good. It also doesn't give an error if I provide more arguments than dependent values. I'd like to find a way of expressing "
generatorFunction
has one argument for each element in
dependencies
, of type
T[DependencyName]
."

I hope this makes sense. Appreciate any help anyone can offer.

Answer Source

You will need to make overloads for each arity (signature). For example, take a look at how Reselect does things

/* one selector */
export function createSelector<S, R1, T>(
  selector: Selector<S, R1>,
  combiner: (res: R1) => T,
): OutputSelector<S, T, (res: R1) => T>;
export function createSelector<S, P, R1, T>(
  selector: ParametricSelector<S, P, R1>,
  combiner: (res: R1) => T,
): OutputParametricSelector<S, P, T, (res: R1) => T>;

/* two selectors */
export function createSelector<S, R1, R2, T>(
  selector1: Selector<S, R1>,
  selector2: Selector<S, R2>,
  combiner: (res1: R1, res2: R2) => T,
): OutputSelector<S, T, (res1: R1, res2: R2) => T>;
export function createSelector<S, P, R1, R2, T>(
  selector1: ParametricSelector<S, P, R1>,
  selector2: ParametricSelector<S, P, R2>,
  combiner: (res1: R1, res2: R2) => T,
): OutputParametricSelector<S, P, T, (res1: R1, res2: R2) => T>;

/* three selectors */
export function createSelector<S, R1, R2, R3, T>(
  selector1: Selector<S, R1>,
  selector2: Selector<S, R2>,
  selector3: Selector<S, R3>,
  combiner: (res1: R1, res2: R2, res3: R3) => T,
): OutputSelector<S, T, (res1: R1, res2: R2, res3: R3) => T>;
export function createSelector<S, P, R1, R2, R3, T>(
  selector1: ParametricSelector<S, P, R1>,
  selector2: ParametricSelector<S, P, R2>,
  selector3: ParametricSelector<S, P, R3>,
  combiner: (res1: R1, res2: R2, res3: R3) => T,
): OutputParametricSelector<S, P, T, (res1: R1, res2: R2, res3: R3) => T>;

// etc...

Make overloads for each number of arguments until a reasonable number (how many are you expecting? 4? 8?) and past that just use a non-constricted generic and let the user input it. If you have more than 8 parameters, it should hurt you to type.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download