Repository navigation
Description
TypeScript Version: 3.6.0-dev.20190727
Search Terms: conditional, generic, inheritance, variance
Code
Prior to the --strictFunctionTypes compiler flag in the strict suite, this previously worked fine.
type GetPropertyNamesOfType<T, RestrictToType> = { [PropertyName in Extract<keyof T, string>]: [T[PropertyName]] extends [RestrictToType] ? PropertyName : never }[Extract<keyof T, string>];
type GetAllPropertiesOfType<T, RestrictToType> = Pick<T, GetPropertyNamesOfType<Required<T>, RestrictToType>>;
export interface IRequestA<T extends object>{
getValue(name: keyof GetAllPropertiesOfType<T, string>): string;
getObject<ObjectType extends object = never>(name: keyof GetAllPropertiesOfType<T, ObjectType>): IRequestA<ObjectType>;
}
interface Base {
BaseString: string;
BaseNumber: number;
BaseObject: Base;
}
interface Derived extends Base {
DerivedString: string;
DerivedNumber: number;
DerivedObject: Base;
}
function main(a: IRequestA<Derived>): void {
const a1: IRequestA<Base> = a;
a1.getValue("BaseString");
a1.getObject<Base>("BaseObject");
}As the result of a suggestion in Issue 28671 the interface was switch to a type. This worked until TypeScript version 3.5 where the assignment again started to fail when inheritance was involved with T.
type GetPropertyNamesOfType<T, RestrictToType> = { [PropertyName in Extract<keyof T, string>]: [T[PropertyName]] extends [RestrictToType] ? PropertyName : never }[Extract<keyof T, string>];
type GetAllPropertiesOfType<T, RestrictToType> = Pick<T, GetPropertyNamesOfType<Required<T>, RestrictToType>>;
export type IRequestB<T extends object> = {
getValue(name: keyof GetAllPropertiesOfType<T, string>): string;
getObject<ObjectType extends object = never>(name: keyof GetAllPropertiesOfType<T, ObjectType>): IRequestB<ObjectType>;
}
interface Base {
BaseString: string;
BaseNumber: number;
BaseObject: Base;
}
interface Derived extends Base {
DerivedString: string;
DerivedNumber: number;
DerivedObject: Base;
}
function main(b: IRequestB<Derived>): void {
const b1: IRequestB<Base> = b;
b1.getValue("BaseString");
b1.getObject<Base>("BaseObject");
}With some changes I got it to work by switching around the allowed property name list:
type GetPropertyNamesOfType<T, RestrictToType> = { [PropertyName in Extract<keyof T, string>]: [T[PropertyName]] extends [RestrictToType] ? PropertyName : never }[Extract<keyof T, string>];
type GetAllPropertiesOfType<T, RestrictToType> = Pick<T, GetPropertyNamesOfType<Required<T>, RestrictToType>>;
export type IRequestC<T extends object> = {
getValue<K extends keyof GetAllPropertiesOfType<T, string>>(name: K): string;
getObject<ObjectType extends object = never>(name: keyof GetAllPropertiesOfType<T, ObjectType>): IRequestC<ObjectType>;
}
interface Base {
BaseString: string;
BaseNumber: number;
BaseObject: Base;
}
interface Derived extends Base {
DerivedString: string;
DerivedNumber: number;
DerivedObject: Base;
}
function main(c: IRequestC<Derived>): void {
const c1: IRequestC<Base> = c;
c1.getValue("BaseString");
c1.getObject<Base>("BaseObject");
}Expected behavior:
I would expect b to be assignable to b1 in the example above to be able to work with the subset of properties that are specific to the base type.
Actual behavior:
The assignment doesn't work or I need to jump through hoops to make it work.
I don't know if I'm just trying to limp by staying one step ahead of type checker improvements that will again cause me to make changes or possibly disallow me from accomplishing what I'm looking for.
I'm also not sure about the inconsistency in the need to update the getValue method versus getObject.
Playground Link:
Demo
Related Issues:
Issue 28671
Issue 24190
Activity
RyanCavanaugh commentedon Jul 31, 2019
Is this the simplest possible repro?
mtreder commentedon Aug 1, 2019
I can remove the extra layer of creating a type to pull property names from via keyof, but still see the error.
Reproduction
mtreder commentedon Aug 1, 2019
This issue sounded related, but I didn't see the error go away when I was using the under dev typescript version:
Issue 31804
RyanCavanaugh commentedon Aug 1, 2019
Anders Hejlsberg (@ahejlsberg) this looks like a possible incorrect variance measurement.
IRequestBshould be covariant inTbut it's being treated as invariant because of theT[PropertyName]usage in the conditional type, but it seems like the check expression in a conditional isn't really a valid variance measurement site? Not surejack-williams commentedon Aug 6, 2019
I think the issue here is with the simultaneous use of
keyof TandT[PropertyName], rather than the conditional type. Repro without conditional types that I think is roughly faithful:Related to #32311 and #32674.
mtreder commentedon Aug 7, 2019
The intent in my example was to pull the property names for a property that matches a given data type. The getValue method should only allow specifying a "name" of a property on T that is of type string (i.e. BaseNumber and BaseObject should be excluded). It is also filtering out property names that aren't of type string, but my example doesn't have any property names that aren't strings. The quoted example above looks to be a different way to express Extract<keyof T, string>.
mtreder commentedon Aug 7, 2019
It is also a need for this to work with a list of objects of a given type. My current workarounds don't seem to allow me to not produce an error.
Example
RyanCavanaugh commentedon Aug 8, 2019
Here's a cough simplified demo that the variance is measured incorrectly relative to a manual instantiation.
IRequestC<Base>andIRequestBaseshould have the same respective relations toIRequestC<Derived>andIRequestDerived.jack-williams commentedon Aug 9, 2019
As check types are related bivariantly I would expect that variance measuring going wrong for the check type would make the measured types more assignable, rather than less. (False-negative)
Conversely, the extends types is invariant so that usually makes measured types less assignable, rather than more. (False-positive).
I think trying to get any meaningful measurements from a conditional type is hard. When the true and false types are unrelated then measurements are just wrong because the conditional type cannot be an order preserving function of its outer type parameters.
My concern is that removing measurements for conditional types would be a breaking change for many people that used
anyas the instantiation for a measured conditional type.2 remaining items
mtreder commentedon Feb 13, 2020
I just wanted to check in on this to see if there was an idea on when this would be addressed. I am trying to determine whether I should look at alternatives for my API.
ahejlsberg commentedon Oct 26, 2020
This looks to be the same issue as #32674. When a type parameter appears in both a co- and contra-variant position within a type, as in
T[keyof T], we measure that type parameter as invariant. This is a conservative measurement and it may indeed not always be the case, as illustrated by my comment here. The only real fix here is to mark such occurrences as unmeasurable and force structural evaluation--but we know there are significant performance penalties associated with that.