Skip to content
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

When possible, remove fully-excluded constituents of a union-constrained type parameter from the false arm of a conditional type #48710

Open
alesmenzel opened this issue Apr 14, 2022 · 6 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@alesmenzel
Copy link

Bug Report

πŸ”Ž Search Terms

narrow type, mapped type, union

πŸ•— Version & Regression Information

Tested on latest version 4.6.2

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

import { ComponentType } from "react"

export type ElementConstraint = keyof JSX.IntrinsicElements | ComponentType<any>

export type ElementProps<Element extends ElementConstraint> =
	Element extends ComponentType<infer P>
	? P : JSX.IntrinsicElements[Element]
//                ^?
// WRONG: Here JSX.IntrinsicElements[Element] complains that Element cannot be used to index JSX.IntrinsicElements eventhoug we know it must be keyof JSX.IntrinsicElements...

πŸ™ Actual behavior

TS doesn't narrow the type and thus throws an error.

πŸ™‚ Expected behavior

When generic is constrained by a union type (e.g. A | B) then the conditional type should narrow the type when checking against one of those types from the union. T extends A ? <here we know it must be A type> : <here we know it must be B type>

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Apr 15, 2022

Simpler:

type IsB<T extends "B"> = unknown;
type F<T extends "A" | "B"> = T extends "A" ? unknown : IsB<T>;

In the general case, it's not sound to narrow the false arm of a conditional type. When the check type fully subsumes one of the constraint's union constituents we could, but that logic just isn't present right now. Regardless, the workaround is straightforward.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Apr 15, 2022
@RyanCavanaugh RyanCavanaugh changed the title Incorrectly narrows union type When possible, remove fully-excluded constituents of a union-constraint type parameter from the false arm of a conditional type Apr 15, 2022
@RyanCavanaugh RyanCavanaugh changed the title When possible, remove fully-excluded constituents of a union-constraint type parameter from the false arm of a conditional type When possible, remove fully-excluded constituents of a union-constrained type parameter from the false arm of a conditional type Apr 15, 2022
@alesmenzel
Copy link
Author

Thanks for the suggestion, but the simpler version has 2 issues

  • T is not used in the first statement
  • IsB<T> shows a squiggly, because TS doesn't narrow the generic T to type "B" so it complains that you cannot use T in isB<T> because the T must be of type "B", which it is, but TS doesn't know it yet. (playground)

@Elias-Graf
Copy link

Elias-Graf commented Oct 4, 2022

Since this is tagged as "awaiting more feedback", I'll quickly leave some (what I believe are simpler, - or at least easier to understand) examples here, from my issue, which was marked as duplicate.

Adding "special cases" to a ObjVal type (generic over Key):

interface Obj {
  foo1: number;
  foo2?: number;
  bar1: string;
  bar2?: string;
}

type SpecialCase = { id: number };
type ObjKey = keyof Obj | "specialCase";

// Trying to handle the "special case" key:
type ObjVal<Key extends ObjKey> = Key extends "specialCase" ? SpecialCase : Obj[Key];
  // Does not work... Error: Type 'Key' cannot be used to index type 'Obj'.(2536)

Playground

Trying to extract the value type, of a particular key, that is only present in one part of the union:

type A = {x: string;};
type B = {y: string;};
type Both = A | B;
// Type '"y"' cannot be used to index type 'T'.(2536)
type X1<T extends Both> = T extends A ? T["x"] : T["y"];
// Type '"x"' cannot be used to index type 'T'.(2536)
type X2<T extends Both> = T extends B ? T["y"] : T["x"];

Playground


I would like it if TypeScript, where it is "sound", at least tried to evaluate the false branch of the conditional type. I don't think what I'm trying to do here is that unreasonable :).

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Oct 7, 2022

Just documenting the workarounds since I'm confirming they exist anyway:

type ObjVal<Key extends ObjKey> = Key extends "specialCase" ? SpecialCase : Obj[Exclude<Key, "specialCase">];

// Make them different so we can check them
type A = {x: "is x";};
type B = {y: "is y";};
type Both = A | B;
type X1<T extends Both> = T extends { x: infer U } | { y: infer U } ? U : never;

// Tests, all ok
type T1 = X1<A>;
type T2 = X1<B>;
type T3 = X1<A | B>;

@RyanCavanaugh
Copy link
Member

type X1<T extends Both> = T extends Partial<Record<"x" | "y", infer U>> ? U : never;

is also possible but probably worse

@Elias-Graf
Copy link

type ObjVal<Key extends ObjKey> = Key extends "specialCase" ? SpecialCase : Obj[Exclude<Key, "specialCase">];

I actually did that, but it became tedious quickly, after needing to add multiple special cases. Especially since the key name has to be typed out twice, but that's nitpicking.

Anyhow, I'm not sure if it has been mentioned before, but I find this a bit nicer:

interface Obj { foo1: number; foo2?: number; bar1: string; bar2?: string; }
type SpecialCase1 = { id_1: number };
type SpecialCase2 = { id_2: number };
type ObjKey = keyof Obj | "specialCase1" | "specialCase2";

type ObjVal<Key extends ObjKey> = Key extends keyof Obj
  ? Obj[Key]
  : Key extends "specialCase1"
  ? SpecialCase1
  : Key extends "specialCase2"
  ? SpecialCase2
  : never;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants