Skip to content

Deferred conditional in value position of mapped type is not re-evaluated when sufficient information is available to pick a branch #42385

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
beezee opened this issue Jan 17, 2021 · 5 comments · Fixed by #42449
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue

Comments

@beezee
Copy link

beezee commented Jan 17, 2021

Bug Report

Per the docs on conditional types, deferred conditionals are expected to be resolved once sufficient context is provided (e.g. at a call-site) such that the compiler can evaluate a single branch of the conditional.

It appears that when the conditional type is in the value position of a mapped type, this does not occur.

🔎 Search Terms

deferred conditional types, mapped types

🕗 Version & Regression Information

  • This is the behavior in every version I tried (all versions on playground), and I reviewed the entire FAQ

⏯ Playground Link

Playground link with relevant code

💻 Code

interface Targets<A> {
    left: A
    right: A
}
type Target = keyof Targets<any>
type Result<F extends Target, A> = Targets<A>[F]

type LR<F extends Target, L, R> = [F] extends ["left"] ? L : R

interface Ops<F extends Target> {
    _f: F
    str: Result<F, string>
    num: Result<F, number>
    lr<I, O>(a: Result<F, I>, o: Result<F, O>): Result<F, LR<F, I, O>>
    dict: <P>(p: {[k in keyof P]: Result<F, P[k]>}) => Result<F, P>
}
const left: Ops<"left"> = {} as any
const right: Ops<"right"> = {} as any

const ok = <F extends Target>(at: Ops<F>) => ({lr: at.lr(at.str, at.num)})
const orphaned = <F extends Target>(at: Ops<F>) => at.dict(ok(at))

const leftOk = ok(left)
const leftOrphaned = orphaned(left)

const rightOk = ok(right)
const rightOrphaned = orphaned(right)

🙁 Actual behavior

Note that the assignments of leftOk and rightOk properly evaluate the conditional type to the branches dictated by the respective arguments passed to ok, with leftOk taking type {lr: string} and rightOk taking type {lr: number}.

However with the same information available (same exact arguments, just passed through one extra layer of call-stack, and a mapped type), leftOrphaned and rightOrphaned do not evaluate the conditional type, both remaining at type {lr: LR<F, string, number>} despite F being determined by the respective arguments passed to orphaned.

🙂 Expected behavior

The types of leftOrphaned and rightOrphaned should match the types of leftOk and rightOk respectively. Application of a type argument to a type parameter (in this case "left" or "right" to F) should apply to sub-expressions in a function body, and those applications should hold under a mapped type.

@RyanCavanaugh RyanCavanaugh added the Bug A bug in TypeScript label Jan 21, 2021
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 4.3.0 milestone Jan 21, 2021
@RyanCavanaugh
Copy link
Member

We're leaking a type parameter here: leftOrphaned.lr has the type LR<F, string, number> where F is unbound. This can result in invalid .d.ts emit too.

@beezee
Copy link
Author

beezee commented Jan 21, 2021

Glad to see your response, thanks. @RyanCavanaugh got any pointers where I might look to familiarize myself if I wanted to take a crack at this particular issue?

@RyanCavanaugh
Copy link
Member

Without knowing the root cause yet, I would still say this is very, very likely to require expertise beyond what we're capable of documenting. Probably there's a type mapper that needs to get passed around somewhere and that's not happening, but I could be way off.

@typescript-bot typescript-bot added the Fix Available A PR has been opened for this issue label Jan 21, 2021
@beezee
Copy link
Author

beezee commented Jan 24, 2021

Thanks very much @weswigham for the quick turnaround on resolution. @RyanCavanaugh I wonder if per your processes this means the fix might make it in sooner, or is the milestone assignment an imposed constraint on timeline? Anything I can do to help see this fix merged and released as soon as it could be?

@RyanCavanaugh
Copy link
Member

If the PR gets merged before the 4.2 RC date (seems likely), it'll be in 4.2. For a long-standing issue with only one report, we wouldn't apply any extra prioritization effort beyond that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants