Search code examples
typescriptgenericstypescript-typingsgeneric-programmingtypescript-generics

How can I write function in TypeScript that apply only string


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


Solution

  • 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