Skip to content

Support contextual type discrimination based on template literal expressions #62203

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2065,6 +2065,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
var autoType = createIntrinsicType(TypeFlags.Any, "any", ObjectFlags.NonInferrableType, "auto");
var wildcardType = createIntrinsicType(TypeFlags.Any, "any", /*objectFlags*/ undefined, "wildcard");
var blockedStringType = createIntrinsicType(TypeFlags.Any, "any", /*objectFlags*/ undefined, "blocked string");
var contextFreeType = createIntrinsicType(TypeFlags.Any, "any", /*objectFlags*/ undefined, "context free");
var errorType = createIntrinsicType(TypeFlags.Any, "error");
var unresolvedType = createIntrinsicType(TypeFlags.Any, "unresolved");
var nonInferrableAnyType = createIntrinsicType(TypeFlags.Any, "any", ObjectFlags.ContainsWideningType, "non-inferrable");
Expand Down Expand Up @@ -41198,10 +41199,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
types.push(isTypeAssignableTo(type, templateConstraintType) ? type : stringType);
}
const evaluated = node.parent.kind !== SyntaxKind.TaggedTemplateExpression && evaluate(node).value;
if (evaluated) {
if (typeof evaluated === "string") {
return getFreshTypeOfLiteralType(getStringLiteralType(evaluated));
}
if (isConstContext(node) || isTemplateLiteralContext(node) || someType(getContextualType(node, /*contextFlags*/ undefined) || unknownType, isTemplateLiteralContextualType)) {
if (isConstContext(node) || isTemplateLiteralContext(node)) {
return getTemplateLiteralType(texts, types);
}
const contextualType = getContextualType(node, /*contextFlags*/ undefined) || unknownType;
if (contextualType === contextFreeType ? every(types, t => isLiteralType(t) || isPatternLiteralPlaceholderType(t)) : someType(contextualType, isTemplateLiteralContextualType)) {
return getTemplateLiteralType(texts, types);
}
return stringType;
Expand Down Expand Up @@ -41651,7 +41656,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (links.contextFreeType) {
return links.contextFreeType;
}
pushContextualType(node, anyType, /*isCache*/ false);
pushContextualType(node, contextFreeType, /*isCache*/ false);
const type = links.contextFreeType = checkExpression(node, CheckMode.SkipContextSensitive);
popContextualType();
return type;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,35 @@ foo({
},
});

type E1 = { d: "main1-sub1"; cb: (x: string) => void };
>E1 : Symbol(E1, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 12, 3))
>d : Symbol(d, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 14, 11))
>cb : Symbol(cb, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 14, 28))
>x : Symbol(x, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 14, 34))

type E2 = { d: "main2-sub2"; cb: (x: number) => void };
>E2 : Symbol(E2, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 14, 55))
>d : Symbol(d, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 15, 11))
>cb : Symbol(cb, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 15, 28))
>x : Symbol(x, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 15, 34))

declare function bar(_: E1 | E2): void;
>bar : Symbol(bar, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 15, 55))
>_ : Symbol(_, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 17, 21))
>E1 : Symbol(E1, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 12, 3))
>E2 : Symbol(E2, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 14, 55))

const someCategory = "main1";
>someCategory : Symbol(someCategory, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 19, 5))

const someSubcategory = "sub1";
>someSubcategory : Symbol(someSubcategory, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 20, 5))

bar({ d: `${someCategory}-${someSubcategory}`, cb: (x) => {} });
>bar : Symbol(bar, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 15, 55))
>d : Symbol(d, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 22, 5))
>someCategory : Symbol(someCategory, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 19, 5))
>someSubcategory : Symbol(someSubcategory, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 20, 5))
>cb : Symbol(cb, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 22, 46))
>x : Symbol(x, Decl(discriminantUsingEvaluatableTemplateExpression.ts, 22, 52))

Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,63 @@ foo({
},
});

type E1 = { d: "main1-sub1"; cb: (x: string) => void };
>E1 : E1
> : ^^
>d : "main1-sub1"
> : ^^^^^^^^^^^^
>cb : (x: string) => void
> : ^ ^^ ^^^^^
>x : string
> : ^^^^^^

type E2 = { d: "main2-sub2"; cb: (x: number) => void };
>E2 : E2
> : ^^
>d : "main2-sub2"
> : ^^^^^^^^^^^^
>cb : (x: number) => void
> : ^ ^^ ^^^^^
>x : number
> : ^^^^^^

declare function bar(_: E1 | E2): void;
>bar : (_: E1 | E2) => void
> : ^ ^^ ^^^^^
>_ : E1 | E2
> : ^^^^^^^

const someCategory = "main1";
>someCategory : "main1"
> : ^^^^^^^
>"main1" : "main1"
> : ^^^^^^^

const someSubcategory = "sub1";
>someSubcategory : "sub1"
> : ^^^^^^
>"sub1" : "sub1"
> : ^^^^^^

bar({ d: `${someCategory}-${someSubcategory}`, cb: (x) => {} });
>bar({ d: `${someCategory}-${someSubcategory}`, cb: (x) => {} }) : void
> : ^^^^
>bar : (_: E1 | E2) => void
> : ^ ^^ ^^^^^
>{ d: `${someCategory}-${someSubcategory}`, cb: (x) => {} } : { d: "main1-sub1"; cb: (x: string) => void; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
>d : "main1-sub1"
> : ^^^^^^^^^^^^
>`${someCategory}-${someSubcategory}` : "main1-sub1"
> : ^^^^^^^^^^^^
>someCategory : "main1"
> : ^^^^^^^
>someSubcategory : "sub1"
> : ^^^^^^
>cb : (x: string) => void
> : ^ ^^^^^^^^^^^^^^^^^
>(x) => {} : (x: string) => void
> : ^ ^^^^^^^^^^^^^^^^^
>x : string
> : ^^^^^^

Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
discriminatedUnionTypesOverlappingDiscriminants1.ts(83,7): error TS2322: Type '{ audience: "earth"; meal: "vegetable_cow"; }' is not assignable to type 'Target'.
Types of property 'meal' are incompatible.
Type '"vegetable_cow"' is not assignable to type 'Custom | "fruit_apple" | "fruit_orange" | "vegetable_spinach" | "vegetable_carrot" | "other_milk" | "other_water"'. Did you mean '"vegetable_carrot"'?
discriminatedUnionTypesOverlappingDiscriminants1.ts(89,7): error TS2322: Type '{ meal: string; audience: "earth"; }' is not assignable to type 'Target'.
Types of property 'audience' are incompatible.
Type '"earth"' is not assignable to type '"mars" | "jupiter"'.
discriminatedUnionTypesOverlappingDiscriminants1.ts(97,7): error TS2322: Type '{ audience: "earth"; meal: "vegetable_cow"; }' is not assignable to type 'Target'.
Types of property 'meal' are incompatible.
Type '"vegetable_cow"' is not assignable to type 'Custom | "fruit_apple" | "fruit_orange" | "vegetable_spinach" | "vegetable_carrot" | "other_milk" | "other_water"'. Did you mean '"vegetable_carrot"'?
discriminatedUnionTypesOverlappingDiscriminants1.ts(103,7): error TS2322: Type '{ meal: string; audience: "earth"; }' is not assignable to type 'Target'.
Types of property 'audience' are incompatible.
Type '"earth"' is not assignable to type '"mars" | "jupiter"'.
discriminatedUnionTypesOverlappingDiscriminants1.ts(111,7): error TS2322: Type '{ audience: "earth"; meal: "vegetable_cow"; }' is not assignable to type 'Target'.
Types of property 'meal' are incompatible.
Type '"vegetable_cow"' is not assignable to type 'Custom | "fruit_apple" | "fruit_orange" | "vegetable_spinach" | "vegetable_carrot" | "other_milk" | "other_water"'. Did you mean '"vegetable_carrot"'?
discriminatedUnionTypesOverlappingDiscriminants1.ts(117,7): error TS2322: Type '{ meal: string; audience: "earth"; }' is not assignable to type 'Target'.
Types of property 'audience' are incompatible.
Type '"earth"' is not assignable to type '"mars" | "jupiter"'.
discriminatedUnionTypesOverlappingDiscriminants1.ts(125,7): error TS2322: Type '{ audience: "earth"; meal: "vegetable_cow" | "vegetable_pig"; }' is not assignable to type 'Target'.
Types of property 'meal' are incompatible.
Type '"vegetable_cow" | "vegetable_pig"' is not assignable to type 'Custom | "fruit_apple" | "fruit_orange" | "vegetable_spinach" | "vegetable_carrot" | "other_milk" | "other_water"'.
Type '"vegetable_cow"' is not assignable to type 'Custom | "fruit_apple" | "fruit_orange" | "vegetable_spinach" | "vegetable_carrot" | "other_milk" | "other_water"'. Did you mean '"vegetable_carrot"'?
discriminatedUnionTypesOverlappingDiscriminants1.ts(131,7): error TS2322: Type '{ meal: string; audience: "earth"; }' is not assignable to type 'Target'.
Types of property 'audience' are incompatible.
Type '"earth"' is not assignable to type '"mars" | "jupiter"'.


==== discriminatedUnionTypesOverlappingDiscriminants1.ts (8 errors) ====
// https://github.com/microsoft/TypeScript/issues/57231

type Food = "apple" | "orange";
type Vegetable = "spinach" | "carrot";
type Other = "milk" | "water";
type Custom = "air" | "soil";

type Target =
| {
audience: "earth";
meal:
| Custom
| `fruit_${Food}`
| `vegetable_${Vegetable}`
| `other_${Other}`;
}
| {
audience: "mars" | "jupiter";
meal: string;
};

const target1: Target = {
audience: "earth",
meal: `vegetable_carrot`,
};

const target2: Target = {
meal: `vegetable_carrot`,
audience: "earth",
};

const typedVegetableWithInitializer: Vegetable = 'carrot';

const target3: Target = {
audience: "earth",
meal: `vegetable_${typedVegetableWithInitializer}`,
};

const target4: Target = {
meal: `vegetable_${typedVegetableWithInitializer}`,
audience: "earth",
};

const typedCarrotWithInitializer: "carrot" = 'carrot';

const target5: Target = {
audience: "earth",
meal: `vegetable_${typedCarrotWithInitializer}`,
};

const target6: Target = {
meal: `vegetable_${typedCarrotWithInitializer}`,
audience: "earth",
};

const carrotInitializer = 'carrot';

const target7: Target = {
audience: "earth",
meal: `vegetable_${carrotInitializer}`,
};

const target8: Target = {
meal: `vegetable_${carrotInitializer}`,
audience: "earth",
};

declare const vegetable: Vegetable;

const target9: Target = {
audience: "earth",
meal: `vegetable_${vegetable}`,
};

const target10: Target = {
meal: `vegetable_${vegetable}`,
audience: "earth",
};

const typedNonVegetableWithInitializer: "cow" | "pig" = "cow";

// error
const target11: Target = {
~~~~~~~~
!!! error TS2322: Type '{ audience: "earth"; meal: "vegetable_cow"; }' is not assignable to type 'Target'.
!!! error TS2322: Types of property 'meal' are incompatible.
!!! error TS2322: Type '"vegetable_cow"' is not assignable to type 'Custom | "fruit_apple" | "fruit_orange" | "vegetable_spinach" | "vegetable_carrot" | "other_milk" | "other_water"'. Did you mean '"vegetable_carrot"'?
audience: "earth",
meal: `vegetable_${typedNonVegetableWithInitializer}`,
};

// error
const target12: Target = {
~~~~~~~~
!!! error TS2322: Type '{ meal: string; audience: "earth"; }' is not assignable to type 'Target'.
!!! error TS2322: Types of property 'audience' are incompatible.
!!! error TS2322: Type '"earth"' is not assignable to type '"mars" | "jupiter"'.
meal: `vegetable_${typedNonVegetableWithInitializer}`,
audience: "earth",
};

const typedCowWithInitializer: "cow" = "cow";

// error
const target13: Target = {
~~~~~~~~
!!! error TS2322: Type '{ audience: "earth"; meal: "vegetable_cow"; }' is not assignable to type 'Target'.
!!! error TS2322: Types of property 'meal' are incompatible.
!!! error TS2322: Type '"vegetable_cow"' is not assignable to type 'Custom | "fruit_apple" | "fruit_orange" | "vegetable_spinach" | "vegetable_carrot" | "other_milk" | "other_water"'. Did you mean '"vegetable_carrot"'?
audience: "earth",
meal: `vegetable_${typedCowWithInitializer}`,
};

// error
const target14: Target = {
~~~~~~~~
!!! error TS2322: Type '{ meal: string; audience: "earth"; }' is not assignable to type 'Target'.
!!! error TS2322: Types of property 'audience' are incompatible.
!!! error TS2322: Type '"earth"' is not assignable to type '"mars" | "jupiter"'.
meal: `vegetable_${typedCowWithInitializer}`,
audience: "earth",
};

const cowInitializer = "cow";

// error
const target15: Target = {
~~~~~~~~
!!! error TS2322: Type '{ audience: "earth"; meal: "vegetable_cow"; }' is not assignable to type 'Target'.
!!! error TS2322: Types of property 'meal' are incompatible.
!!! error TS2322: Type '"vegetable_cow"' is not assignable to type 'Custom | "fruit_apple" | "fruit_orange" | "vegetable_spinach" | "vegetable_carrot" | "other_milk" | "other_water"'. Did you mean '"vegetable_carrot"'?
audience: "earth",
meal: `vegetable_${cowInitializer}`,
};

// error
const target16: Target = {
~~~~~~~~
!!! error TS2322: Type '{ meal: string; audience: "earth"; }' is not assignable to type 'Target'.
!!! error TS2322: Types of property 'audience' are incompatible.
!!! error TS2322: Type '"earth"' is not assignable to type '"mars" | "jupiter"'.
meal: `vegetable_${cowInitializer}`,
audience: "earth",
};

declare const nonVegetable: "cow" | "pig";

// error
const target17: Target = {
~~~~~~~~
!!! error TS2322: Type '{ audience: "earth"; meal: "vegetable_cow" | "vegetable_pig"; }' is not assignable to type 'Target'.
!!! error TS2322: Types of property 'meal' are incompatible.
!!! error TS2322: Type '"vegetable_cow" | "vegetable_pig"' is not assignable to type 'Custom | "fruit_apple" | "fruit_orange" | "vegetable_spinach" | "vegetable_carrot" | "other_milk" | "other_water"'.
!!! error TS2322: Type '"vegetable_cow"' is not assignable to type 'Custom | "fruit_apple" | "fruit_orange" | "vegetable_spinach" | "vegetable_carrot" | "other_milk" | "other_water"'. Did you mean '"vegetable_carrot"'?
audience: "earth",
meal: `vegetable_${nonVegetable}`,
};

// error
const target18: Target = {
~~~~~~~~
!!! error TS2322: Type '{ meal: string; audience: "earth"; }' is not assignable to type 'Target'.
!!! error TS2322: Types of property 'audience' are incompatible.
!!! error TS2322: Type '"earth"' is not assignable to type '"mars" | "jupiter"'.
meal: `vegetable_${nonVegetable}`,
audience: "earth",
};

Loading
Loading