rssfrncs
rssfrncs

Reputation: 1011

Generic function and index types

I'm looking to implement a generic function which accepts a single type argument "T" which is an object. It uses keyof T to specify the first argument to the function "property". Now the part I can't get working, I want to use the type of T[typeof property] to specify the type of the second argument to the function "value".

This gets me close to the ideal usage,

type Person = {
  name: string;
  age: number;
};

function filter<T>(property: keyof T, value: T[typeof property]) {

 return `${property} ${value}`

}

filter<Person>("name", 123);

With the above we get type checking for the "property" argument, correctly restricting it to only "name" or "age". Unfortunately for the value argument it accepts either a number or string, so it seems to be creating a union of all the keyof types of Person.

Any ideas?

Upvotes: 2

Views: 509

Answers (2)

Daniel P&#233;rez
Daniel P&#233;rez

Reputation: 1992

This way you don't have to change your code

type Fn<T> = <K extends keyof T>(prop: K, value: T[K]) =>  string;

function filter(property: string, value: any) {
  return `${property} ${value}`;
}

type Person = {
  name: string;
  age: number;
};

const result = (filter as Fn<Person>)("name", 123);

Upvotes: 1

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249616

You need to create a relation between the two parameters, after all you could potentially be passing in multiple keys, and multiple values, why would the compiler asume that value should have the type of the key specified in the property parameter.

To create the relation you will need an extra type parameter to represent the property as a literal type

function filter<T, K extends keyof T>(property: K, value: T[K]) {
   return `${property} ${value}`
}

filter<Person, "name">("name", 123); // error
filter<Person, "age">("age", 123); // ok

The problem with the above implementation is that you have to specify the additional type parameter since typescript does not support partial type inference. (Hopefuly it will soon be possible as this issue proposes, it's slated for January 2018 but is has been pushed back several times)

To fix this inconvenience we can create a function that returns a function and fix the T parameter in the first call and let K be inferred in the second call

function filter<T>() {
  return function <K extends keyof T>(property: K, value: T[K]) {
    return `${property} ${value}`
  }

}
filter<Person>()("name", 123); // error
filter<Person>()("age", 123); // ok

Or depending on what you plan to do with filter you can leave the function with two type parameters and use the return type as the source for the inference of T:

function filter<T, K extends keyof T>(property: K, value: T[K]) {
  return `${property} ${value}` as IFiler<T>
}

type IFiler<T> = string & { _isFilterFor: T } 
class Query<T> {
  addFilter(f: IFiler<T>) {

  }
}

var q = new Query<Person>();
// T will be inferred to Person in filter because we assign in to a parameter expecting IFilter<Person>
q.addFilter(filter('age', "")) //error 
q.addFilter(filter('age', 12)) //ok 
// Same as above but assigning to a variable:
var f: IFiler<Person> = filter('age', 12);
var f2: IFiler<Person> = filter('age', "12");

Upvotes: 1

Related Questions