4

Let's say I have the generic function:

function f<T, K extends keyof T>(obj: T, key: K) {
   ...
}

I would like to enforce the type of T[K] so I could perform type specific actions. For example, using string:

function f<T, K extends keyof T>(obj: T, key: K): string {
    return obj[key].toLowerCase();
}

Is this at all possible without casting things to any?

Edit: To clarify, I'm looking for the ability to disallow certain keys based on the resulting type. Using the example above, something like:

f({a: 123, b: 'abc'}, 'b') //No errors
f({a: 123, b: 'abc'}, 'a') //Typescript error, T['a'] is not string
TomerKr
  • 43
  • 1
  • 6

2 Answers2

6

To restrict property names to only those having string value types you can use conditional types in conjunction with mapped types:

type StringProperties<T> = { [K in keyof T]: T[K] extends string ? K : never }[keyof T];

declare function f<T>(obj: T, key: StringProperties<T>): string;

f({a: 123, b: 'abc'}, 'b') // No errors
f({a: 123, b: 'abc'}, 'a') // Error: Argument of type '"a"' is not assignable to parameter of type '"b"'.

Playground

Aleksey L.
  • 35,047
  • 10
  • 74
  • 84
1

You can enforce the property types by adding an indexed type, forcing all properties to be of one type:

interface AllStringProps {
   [key: string]: string;
}

function f<T extends AllStringProps, K extends keyof T>(obj: T, key: K): string {
    return obj[key].toLowerCase();
}

However this contstrains you to use an index type that accepts any string as property.

To add a little bit more safety you could change the snippet to

type AllStringProps<T> = {
   [key in keyof T]: string;
}

function f<T extends AllStringProps<T>, K extends keyof T>(obj: T, key: K): string {
    return obj[key].toLowerCase();
}

Here the type AllStringProps enforces that all keys available in T must be of type string. The snippet before forced all possible properties must be of type string.

In your use case, not a big difference, but I always prefer the smallest possible constraint.

Update to a question in the comments

Lets say, we only want to allow a subset of T's keys.

We could start by defining all keys which we want to allow:

type AllowedKeys = { "s1" , "s2" };

type AllStringProps<AllowedKeys> = {
   [key in keyof AllowedKeys]: string;
}

function f<T extends AllStringProps<AllowedKeys>, 
           K extends keyof AllowedKeys>(obj: T, key: K): string {
    return obj[key].toLowerCase();
}

The snippet above would allow passing the following object:

let t1 = {
    s1: "Hello",
    s2: "World",
    other: 4
}

But not this one

let t1 = {
    s1: "Hello",
    s2: 6,
    other: 4
}
Iqon
  • 1,920
  • 12
  • 20
  • This would force T to be have an index type though. Which prevents something like `f({a: 1, b: 'foo'}, 'b')` from compiling. – TomerKr Jul 23 '18 at 10:19
  • Yes exactly, as you allow all keys of T, you need to enforce that all keys return string. – Iqon Jul 23 '18 at 10:23
  • You could enforce only a subset of T's keys to be of type string. I'll update the answer to include an example of that – Iqon Jul 23 '18 at 10:24
  • Thanks. I think my question is a bit unclear. What I would like is to not allow all keys of T, only those such that T[K] is of type string (or whatever other specific type). I'll update the question with a clarification. – TomerKr Jul 23 '18 at 10:29