0

I want to write a generic function that takes as argument the name of the property of the generic type. I need to make TypeScript assert that the value of the property is of a specific type. Consider the following simplified code:

interface MyObject {
 myProp: number;
 mySecondProp: string;
 myOtherProp: string;
 myFlagProp: boolean;
}

const doStuff<T> = (obj: T, propName: SomeType) => { /***/ }

I can extract all the properties of a specific type (e.g. string) but then the compiler would not know exactly the type of the property:

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

const doStuff<T> = (obj: T, propName: StringProps<T>) => { 
  obj[propName].indexOf("a"); // Property 'indexOf' does not exist on type 'T[{ [K in keyof T]: T[K] extends string ? K : never; }[keyof T]]'
}

What can I do to make the compiler understand that it should only accept string property names and that the value is always of type string?

Arthur
  • 3,056
  • 7
  • 31
  • 61
Andrei V
  • 7,306
  • 6
  • 44
  • 64
  • 1
    [Relevant Github comment](https://github.com/microsoft/TypeScript/issues/30728#issuecomment-479542352) – jcalz Oct 31 '19 at 14:52

1 Answers1

4

Typescript isn't generally great at deducting things about conditional types that still contain unresolved type parameters. So while when you call StringProps<T> is easily resolved to whatever the string keys are for T inside the function the compiler will not attempt to sense of T[StringProps<T>].

You can specify the parameters in another way. You can use another type parameter for the property key and specify that T must be a Record<K, string>. This will be simpler for the compiler to reason about:

type StringProps<T> = { [K in keyof T]: T[K] extends string ? K : never }[keyof T];
function doStuff<T>(obj: T, propName: StringProps<T>): void
function doStuff<K extends PropertyKey, T extends Record<K, string>>(obj: T, propName: K): void { 
    obj[propName].indexOf("a"); 
}

Play

You will notice that I kept your signature as well, that one behaves better for the intelisense on the call site, the new signature behaves better for the implementation.

Depending on how much indexing you do in the function you might be better of just using an assertion:

type StringProps<T> = { [K in keyof T]: T[K] extends string ? K : never }[keyof T];
function doStuff<T>(obj: T, propName: StringProps<T>): void{ 
    (obj[propName] as unknown as string).indexOf("a"); 
}
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357