2

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.

1
  • I'm pretty sure it's not possible to do it in an universal way. Instead you should create separate overloads for 1,2,3, ... parameters. The maximum number should match your needs. Commented Sep 29, 2017 at 21:11

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

2 Comments

AH! RESELECT! I knew there was somewhere in my app where I saw this exact pattern play out beautifully but I just could not remember where. I don't use selectors often enough. Thank you! This is excellent. I'll give this a shot on Monday but it's what I was hoping for.
@subvertallchris, beautiful? Excellent? Are you sure? This looks like a messy ugly workaround. I don't think there is a better way of doing this, unfortunately. This thing definitely needs improvement in Typescript.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.