Skip to content

Type of mapped or conditional type member incorrectly tracked #51588

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wbt opened this issue Nov 18, 2022 · 5 comments
Closed

Type of mapped or conditional type member incorrectly tracked #51588

wbt opened this issue Nov 18, 2022 · 5 comments
Labels
Duplicate An existing issue was already created

Comments

@wbt
Copy link

wbt commented Nov 18, 2022

Bug Report

🔎 Search Terms

2344 mapped conditional keyof member

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about "Common 'Bugs' That Aren't Bugs"
  • Nightly version as of test: v5.0.0-dev.20221118

⏯ Playground Link

Playground link with relevant code (Covers all three of the examples below)

💻 Code (3 examples)

//SIMPLER OBJECT VERSION (2345 error): ============================================================
type RacerDetailsObjMap = {
    car: {
        make: string;
        model: string;
        VIN: string;
    };
    horse : {
        usesMetalShoes: string;
        age: string;
        name: string;
    }
}
type RacerDetailsObj<K extends keyof RacerDetailsObjMap> = RacerDetailsObjMap[K];
declare function takesString(arg0: string) : string;
export const demoFn = function<
    K extends keyof RacerDetailsObjMap,
    F extends keyof RacerDetailsObj<K> & string
> (
    arg0: RacerDetailsObj<K>[F]
) {
    //Error ts(2345): Type 'RacerDetailsObj<K>[string]' is not assignable to type 'string'.
    return takesString(arg0);
}
//OBJECT VERSION: More realistic to inspiring example, allows for more possible values============
type RacerDetailsAccessorObjMap = {
    car: {
        usesLiquidFuel: (id: number) => boolean;
        weight: (id: number) => number;
        VIN: (id: number) => string;
    };
    horse : {
        usesMetalShoes: (id: number) => boolean;
        age: (id: number) => number;
        name: (id: number) => string;
    }
};
type RacerDetailsAccessorObj<K extends keyof RacerDetailsAccessorObjMap> = RacerDetailsAccessorObjMap[K];
type ReturnFinderObj<
    K extends keyof RacerDetailsAccessorObjMap,
    F extends keyof RacerDetailsAccessorObj<K> & string
> = ReturnType<RacerDetailsAccessorObj<K>[F]>;
//TS(2344) error on line above:
//Type 'RacerDetailsAccessorObj<K>[string]' is not assignable to type '(...args: any) => any'.
//Each possible value of K, hardcoded, works fine though:
type ReturnFinderC<
    F extends keyof RacerDetailsAccessorObj<'car'> & string
> = ReturnType<RacerDetailsAccessorObj<'car'>[F]>;
type ReturnFinderH<
    F extends keyof RacerDetailsAccessorObj<'horse'> & string
> = ReturnType<RacerDetailsAccessorObj<'horse'>[F]>;
//BOOLEAN VERSION: Uses conditional structure, which sometimes works differently but not here=========
type RacerDetailsAccessor<IsCar extends boolean> = IsCar extends true ? {
    usesLiquidFuel: (id: number) => boolean;
    weight: (id: number) => number;
    VIN: (id: number) => string;
} : {
    usesMetalShoes: (id: number) => boolean;
    age: (id: number) => number;
    name: (id: number) => string;
};
type ReturnFinder<
    IsCar extends boolean,
    F extends keyof RacerDetailsAccessor<IsCar> & string
> = ReturnType<RacerDetailsAccessor<IsCar>[F]>;
//TS(2344) error on line above:
//Type 'RacerDetailsAccessor<IsCar>[string]' is not assignable to type '(...args: any) => any'.
//Each possible value of IsCar, hardcoded, works fine though:
type ReturnFinderT<
    F extends keyof RacerDetailsAccessor<true> & string
> = ReturnType<RacerDetailsAccessor<true>[F]>;
type ReturnFinderF<
    F extends keyof RacerDetailsAccessor<false> & string
> = ReturnType<RacerDetailsAccessor<false>[F]>;

🙁 Actual behavior

Errors as noted, e.g. in the middle one:

Type 'RacerDetailsAccessorObj[string]' is not assignable to type '(...args: any) => any'.

even though any K and F which satisfy the type parameter constraints will lead RacerDetailsAccessorObj<K>[F] to extend (...args: any) => any as required by ReturnType<>. In the Simpler Object Version (shown first) you can see the same effect with a different error code, where the constraint comes from a parameter type to a function instead of another type.

Removing & string changes the penultimate line of the error stack to include something like this:

Type 'RacerDetailsAccessorObj[string] | RacerDetailsAccessorObj[number] | RacerDetailsAccessorObj[symbol]' is not assignable to type '(...args: any) => any'.

Overall, it looks here like type F is being interpreted in an overbroad sense, as if it were keyof <anything> instead of keyof <something much more specific>. It's ignoring the fact that there are only three valid values for F (which three values those are depends on K). When K is hardcoded rather than a generic type parameter, it works as shown, but hardcoding everything is not a maintainable solution in context.

🙂 Expected behavior

All three examples compile without error, even when K is a constrained generic type parameter instead of hardcoded. F can still be interpreted as being limited to one of the valid values for a keyof that object type, and TypeScript can see that all possible values match the constraint.

@RyanCavanaugh
Copy link
Member

It looks like you need #48992

@fatcerberus
Copy link

fatcerberus commented Nov 18, 2022

Note: IsCar can be boolean, i.e. true | false which will take both branches of the conditional type. This is also true for the map version - you can end up with multiple keys in a union. So you might also need #27808

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Nov 18, 2022
@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@wbt
Copy link
Author

wbt commented Nov 21, 2022

I don't think this is the same as #48992 because I'm not doing a test to only get keys where the property matches a particular type value: I'm fetching an object member and trying to use a member of that type. The types are hardcoded in this example and the selection is by name, not by the type. TS should be able to figure out that the types match (avoiding the reported error) without my having to specify what those types are. In the first example, I should not have to specify that I'm selecting KeysOfType string (so as to provide some generality), but TypeScript should be able to figure out in that first example that arg0 will be a string regardless of the value of K (within the constraints already specified on the allowed values of K).

@wbt
Copy link
Author

wbt commented Nov 21, 2022

Here is another example of what I think is likely a duplicate of this one:

type SchoolGroupings = {
	Classroom: {
		studentNames: string[];
		teacherNames: string[];
	}
	SportsTeam: {
		athleteNames: string[];
		coachNames: string[];
		coordinatorNames: string[];
	}
}
declare function maybeGetStrings<SG extends keyof SchoolGroupings>() : 
	SchoolGroupings[SG][keyof SchoolGroupings[SG]] | undefined | null;
export const demo = function<SG extends keyof SchoolGroupings>() {
	const maybeStrings = maybeGetStrings<SG>();
	if((typeof maybeStrings !== 'undefined') && (maybeStrings !== null)) {
		maybeStrings //has a problematic `& ({} | undefined)` at the end of the type
	}
}

I would expect the narrowing to leave just SchoolGroupings[SG][keyof SchoolGroupings[SG]] as the type of maybeStrings in the last primary line, and in this toy example to have that simplify further to string[]. However, there is a strange & ({} | undefined) at the end of the type, preventing doing a lot of things with mabyeStrings that would be interesting. For example, I can't call maybeStrings.length because Object is possibly 'undefined'.ts(2532), even though this is within a type-guarded conditional block that excludes undefined.

If I hard-code the type parameter to maybeGetStrings(), the typing errors go away, but that's not feasible or maintainable to implement in the motivating context.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants