1

I need the function that allow pass only keys if the value in object has string type:

type GetNames<FromType, KeepType = any, Include = true> = {
    [K in keyof FromType]: 
        FromType[K] extends KeepType ? 
            Include extends true ? K : 
            never : Include extends true ? 
            never : K
}[keyof FromType];

const functionOnlyForStrings = <T>(obj: T, key: GetNames<T, string>) => {
    const t = obj[key]
    // do something with strings
    return t.toUpperCase()
}


const testObj: {a: string, b: number} = {a: 'test', b: 123}

const test = functionOnlyForStrings(testObj, 'a')
const wrongParam = functionOnlyForStrings(testObj, 'b')

In lines:

const test = functionOnlyForStrings(testObj, 'a') 
const wrongParam = functionOnlyForStrings(testObj, 'b') // here I get an error message

Everything works great. If I pass b key than TS shows me an error.

But problem in function functionOnlyForStrings. Inside this function TS doesn't know that obj[key] is always string. And show me the error:

Property 'toUpperCase' does not exist on type 'T[{ [K in keyof T]: T[K] extends string ? K : never; }[keyof T]]'.

Playground

2
  • Are you looking for index signature? Type for { [index:string] : string }. Commented Oct 11, 2019 at 13:38
  • I'm not sure that helps. For now I have to add some type gruars to check if the param inside the functionOnlyForStrings is strings, but it is alway string. How should I describe it for TS? Commented Oct 11, 2019 at 13:41

1 Answer 1

2

This is basically a design limitation of TypeScript, at least as of now.

There's only so much we can expect the compiler to understand about manipulating conditional types that depend on generic type parameters. Maybe the compiler could be made to specifically check that T[T[K] extends U ? K : never] is assignable to U. But it would cost something in terms of type checker complexity and compile time, and any benefit would only be seen by some fraction of users who specifically do this kind of thing. It could be worth it, but I wouldn't hold my breath.

Meanwhile, there are two general ways for you to deal with this. One: a judicious use of a type assertion to tell the compiler that it is not as clever as you:

const functionOnlyForStrings = <T>(obj: T, key: GetNames<T, string>) => {
    const t = obj[key] as any as string;  // I'm smarter than you, compiler! 🤓
    return t.toUpperCase()
}

Two: walk the compiler through the type safety of the situation by giving it some generic types that it does properly check:

const functionOnlyForStrings = <
    T extends Record<K, string>, // constrain T to be a function of K
    K extends GetNames<T, string> // constrain K to be a function of T
>(obj: T, key: K) => {
    const t = obj[key]; // inferred as T[K]
    return t.toUpperCase() // no error
}

That works because the compiler already understands that {[P in K]: V}[K] will be assignable to V, and thus T[K] will be assignable to string.

Hope that helps; good luck!

Link to code

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

1 Comment

Thank you for such great answer!

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.