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

2 Answers 2

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

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

1 Comment

Popping in 2.5 years later to say thank you for saving me a lot of frustration!
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
}

4 Comments

This would force T to be have an index type though. Which prevents something like f({a: 1, b: 'foo'}, 'b') from compiling.
Yes exactly, as you allow all keys of T, you need to enforce that all keys return string.
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
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.

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.