3

The best way for me to ask this question is to give an example.

Let's say I want to implement a method which takes three parameters, like so:

customFilter<T>(values: T[], filterString: string, propName: string) any[] {
    return values.filter((value) => value[propName].includes(filterString));
}

In this case, I want to ensure that T is a type which has a property propName which evaluates to a string (or, at the very least, something which implements an includes method).

I've considered writing an interface to use as the first parameter type, but I feel that is just my C# background peeking through. Since the interface would require me to, essentially, hardcode the property name, I don't think that's the right approach.

I know it involves the use of keyof.

I have worked around the issue by doing a quick check in the method:

typeof(values[propName].includes) === 'function'

That, though, feels too "JavaScripty", and, again, I feel like there's probably something in TypeScript that can do this for me.

I know doing something along the lines of this answer would also work, but that still feels very JavaScript.

b4ux1t3
  • 439
  • 1
  • 6
  • 21

1 Answers1

4

Looking at the example code you provided, you want to filter the values argument against a filterString of which you apply to a specified propName that should exist in generic T, but requires the value to be an array.

So we can highlight a few points that we should achieve:

i) We will need to constrain T to have a property that extends an Array. This will be a slight issue because ts has no direct way of defining an interface with at least one property of a certain type. We will provide a cheat though that will maintain an indirect constraint.

ii) So now we move to the args. Firstly, we want the filterString to extend the generic type of the array you are filtering. For instance, say you want to filter a propName whose value is an Array<string>, so then we should expect filterString argument to be of type string right.

iii) With T constrained we will then need to define the argument propName to be keys of T whose values are arrays. i.e. string types.

The implementation of your function definition should stay the same, however we may rearrange the arguments for the order of definitions sake. So let us get coding:

We will first define a type that can pick out the properties from T that are of a specific type. please refer to @jcalz solution to defining such an interface.

type Match<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];

So we define the function as described in the steps above

// we define these types that will filter keys whose values are arrays
type HasIncludes<T> = Match<T, { includes: (x: any) => boolean }>;
// and a type that will extract the generic type of the array value
type IncludeType<T, K extends keyof T> =  T[K] extends { includes: (value: infer U) => boolean } ? U : never;

// we then apply then apply them to the args
function customFilter<T, K extends HasIncludes<T>>(values: T[], propName: K, filterString: IncludeType<T, K>) {
    // so here we have to type cast to any as the first cheat because ts
    // is unable to infer the types of T[K] which should be of type array

    return values.filter((value) => (value[propName] as any)
      .includes(filterString))
}

With the definition done we define some tests

const a = [{ age: 4, surname: 'something' }];
const b = [{
    person: [{ name: 'hello' }, { name: 'world' }]
}];

// so in this case you get the indirect error discussed in that any key
// you refer will not meet the requirement of the argument constraint
const c = [{ foo: 3 }]

// note the generic is implicity inferred by its usage
const result = customFilter(b, 'person', { name: 'type' }); // works as expected
const result = customFilter(a, 'age', 2); // ts error because age is not array

Here is my playground so you can tinker in case you want to test specific cases.

bloo
  • 1,416
  • 2
  • 13
  • 19
  • 1
    That's excellent! It looks like I had most of the pieces, I just couldn't get them into the right place. I ended up starting to build the `Match` type you demonstrated (though much less elegantly), but couldn't figure out how to make sure I'd get the right return type. It didn't occur to me that I needed a *third* type. I'm going to play with it a little and get it into my project, well done and thank you! – b4ux1t3 Sep 18 '21 at 12:10