I've been trying to make a generic function which receives and object T and receives a string property name of that object T.
I used https://www.typescriptlang.org/docs/handbook/advanced-types.html as an example (section: Distributive conditional types)
I've come up with a solution that works without generics, but when I change the explicit types to a generic type typescript won't compile.
This is the non-generic version:
export type TypedPropertyNames<T, P> = { [K in keyof T]: T[K] extends P ? K : never }[keyof T];
export type StringPropertyNames<T> = TypedPropertyNames<T, string>;
interface Test {
test: string;
}
function non_generic(form: Test, field: StringPropertyNames<Test>): string {
return form[field];
}
This works.
Now when I change the Test interface into a generic argument it won't compile anymore.
export type TypedPropertyNames<T, P> = { [K in keyof T]: T[K] extends P ? K : never }[keyof T];
export type StringPropertyNames<T> = TypedPropertyNames<T, string>;
function generic<T>(form: T, field: StringPropertyNames<T>): string {
return form[field]; // This won't compile
}
Is this expected behaviour? Or is this a typescript bug? Can anyone point me in the direction of making the generic version work (without any hacks)
Update 1:
Compilation error:
Type 'T[{ [K in keyof T]: T[K] extends string ? K : never; }[keyof T]]' is not assignable to type 'string'.
The compiler generally cannot determine assignability of unresolved conditional types (that is, conditional types which cannot be eagerly evaluated because at least one of the
T
orU
inT extends U ? V : W
is not yet fully specified).This is more of a design limitation than a bug (see Microsoft/TypeScript#30728); the compiler is not going to be as smart as a human being (note to self: come back here when the machine uprising happens and edit this) so we shouldn't expect it to just "notice" that
T[TypedPropertyName<T,P>] extends P
should always be true. We could write a particular heuristic algorithm to detect the situation and perform the desired reduction, but it would have to be able to run very quickly so that it doesn't degrade compile times for the 99% of the time when it wouldn't be useful.That really depends on what you consider a hack. The absolute simplest thing to do is to use a type assertion, which is explicitly intended for times when you know something is type safe but the compiler isn't able to figure it out:
Or you could try to lead the compiler through the steps necessary to understand that what you're doing is safe. In particular, the compiler does understand that
Record<K, V>[K]
is assignable toV
(whereRecord<K, V>
is defined in the standard library as a mapped type whose keys are inK
and whose values are inV
). You can therefore constrain the typeT
like this:Now the compiler is happy. And the constraint
T extends Record<StringPropertyNames<T>, string>
isn't really a constraint at all, since any object type will conform to it (e.g.,{a: string, b: number}
extendsRecord<'a', string>
). So you should be able to use it anywhere you use the original definition (for concrete typesT
anyway):Are those hacks? ♂️ Okay, hope that helps. Good luck!