From 03951f2ddf727f6e697972a4e9589803229564b7 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 13 Jun 2023 13:57:51 -0300 Subject: [PATCH] Consider index signatures as optional properties in contextual type discrimination (#54596) --- src/compiler/checker.ts | 11 ++-- .../discriminatedUnionWithIndexSignature.js | 44 ++++++++++++++ ...scriminatedUnionWithIndexSignature.symbols | 59 +++++++++++++++++++ ...discriminatedUnionWithIndexSignature.types | 55 +++++++++++++++++ .../discriminatedUnionWithIndexSignature.ts | 29 +++++++++ 5 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 tests/baselines/reference/discriminatedUnionWithIndexSignature.js create mode 100644 tests/baselines/reference/discriminatedUnionWithIndexSignature.symbols create mode 100644 tests/baselines/reference/discriminatedUnionWithIndexSignature.types create mode 100644 tests/cases/compiler/discriminatedUnionWithIndexSignature.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 2874a6f6d3a87..bdf2dff585a47 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -10165,8 +10165,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return prop ? getTypeOfSymbol(prop) : undefined; } - function getTypeOfPropertyOrIndexSignature(type: Type, name: __String): Type { - return getTypeOfPropertyOfType(type, name) || getApplicableIndexInfoForName(type, name)?.type || unknownType; + function getTypeOfPropertyOrIndexSignature(type: Type, name: __String, addOptionalityToIndex: boolean): Type { + let propType; + return getTypeOfPropertyOfType(type, name) || + (propType = getApplicableIndexInfoForName(type, name)?.type) && addOptionality(propType, /*isProperty*/ true, addOptionalityToIndex) || + unknownType; } function isTypeAny(type: Type | undefined) { @@ -22700,7 +22703,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { let matched = false; for (let i = 0; i < types.length; i++) { if (include[i]) { - const targetType = getTypeOfPropertyOfType(types[i], propertyName); + const targetType = getTypeOfPropertyOrIndexSignature(types[i], propertyName, /*addOptionalityToIndex*/ true); if (targetType && related(getDiscriminatingType(), targetType)) { matched = true; } @@ -27013,7 +27016,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { propType = removeNullable && optionalChain ? getOptionalType(propType) : propType; const narrowedPropType = narrowType(propType); return filterType(type, t => { - const discriminantType = getTypeOfPropertyOrIndexSignature(t, propName); + const discriminantType = getTypeOfPropertyOrIndexSignature(t, propName, /*addOptionalityToIndex*/ false); return !(discriminantType.flags & TypeFlags.Never) && !(narrowedPropType.flags & TypeFlags.Never) && areTypesComparable(narrowedPropType, discriminantType); }); } diff --git a/tests/baselines/reference/discriminatedUnionWithIndexSignature.js b/tests/baselines/reference/discriminatedUnionWithIndexSignature.js new file mode 100644 index 0000000000000..04b613995c71d --- /dev/null +++ b/tests/baselines/reference/discriminatedUnionWithIndexSignature.js @@ -0,0 +1,44 @@ +//// [tests/cases/compiler/discriminatedUnionWithIndexSignature.ts] //// + +//// [discriminatedUnionWithIndexSignature.ts] +export interface UnionAltA { + type?: 'text'; +} + +export interface UnionAltB { + type?: 'image' | 'video' | 'document'; +} + +export type ValueUnion = UnionAltA | UnionAltB; + +export type MapOrSingleton = + | { + [key: string]: ValueUnion; + } + | ValueUnion; + +const withoutAsConst: MapOrSingleton = { + 1: { + type: 'text' /*as const*/, + }, +}; + +const withAsConst: MapOrSingleton = { + 1: { + type: 'text' as const, + }, +}; + +//// [discriminatedUnionWithIndexSignature.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +var withoutAsConst = { + 1: { + type: 'text' /*as const*/, + }, +}; +var withAsConst = { + 1: { + type: 'text', + }, +}; diff --git a/tests/baselines/reference/discriminatedUnionWithIndexSignature.symbols b/tests/baselines/reference/discriminatedUnionWithIndexSignature.symbols new file mode 100644 index 0000000000000..92f734d0d6bb1 --- /dev/null +++ b/tests/baselines/reference/discriminatedUnionWithIndexSignature.symbols @@ -0,0 +1,59 @@ +//// [tests/cases/compiler/discriminatedUnionWithIndexSignature.ts] //// + +=== discriminatedUnionWithIndexSignature.ts === +export interface UnionAltA { +>UnionAltA : Symbol(UnionAltA, Decl(discriminatedUnionWithIndexSignature.ts, 0, 0)) + + type?: 'text'; +>type : Symbol(UnionAltA.type, Decl(discriminatedUnionWithIndexSignature.ts, 0, 28)) +} + +export interface UnionAltB { +>UnionAltB : Symbol(UnionAltB, Decl(discriminatedUnionWithIndexSignature.ts, 2, 1)) + + type?: 'image' | 'video' | 'document'; +>type : Symbol(UnionAltB.type, Decl(discriminatedUnionWithIndexSignature.ts, 4, 28)) +} + +export type ValueUnion = UnionAltA | UnionAltB; +>ValueUnion : Symbol(ValueUnion, Decl(discriminatedUnionWithIndexSignature.ts, 6, 1)) +>UnionAltA : Symbol(UnionAltA, Decl(discriminatedUnionWithIndexSignature.ts, 0, 0)) +>UnionAltB : Symbol(UnionAltB, Decl(discriminatedUnionWithIndexSignature.ts, 2, 1)) + +export type MapOrSingleton = +>MapOrSingleton : Symbol(MapOrSingleton, Decl(discriminatedUnionWithIndexSignature.ts, 8, 47)) + + | { + [key: string]: ValueUnion; +>key : Symbol(key, Decl(discriminatedUnionWithIndexSignature.ts, 12, 9)) +>ValueUnion : Symbol(ValueUnion, Decl(discriminatedUnionWithIndexSignature.ts, 6, 1)) + } + | ValueUnion; +>ValueUnion : Symbol(ValueUnion, Decl(discriminatedUnionWithIndexSignature.ts, 6, 1)) + +const withoutAsConst: MapOrSingleton = { +>withoutAsConst : Symbol(withoutAsConst, Decl(discriminatedUnionWithIndexSignature.ts, 16, 5)) +>MapOrSingleton : Symbol(MapOrSingleton, Decl(discriminatedUnionWithIndexSignature.ts, 8, 47)) + + 1: { +>1 : Symbol(1, Decl(discriminatedUnionWithIndexSignature.ts, 16, 40)) + + type: 'text' /*as const*/, +>type : Symbol(type, Decl(discriminatedUnionWithIndexSignature.ts, 17, 8)) + + }, +}; + +const withAsConst: MapOrSingleton = { +>withAsConst : Symbol(withAsConst, Decl(discriminatedUnionWithIndexSignature.ts, 22, 5)) +>MapOrSingleton : Symbol(MapOrSingleton, Decl(discriminatedUnionWithIndexSignature.ts, 8, 47)) + + 1: { +>1 : Symbol(1, Decl(discriminatedUnionWithIndexSignature.ts, 22, 37)) + + type: 'text' as const, +>type : Symbol(type, Decl(discriminatedUnionWithIndexSignature.ts, 23, 8)) +>const : Symbol(const) + + }, +}; diff --git a/tests/baselines/reference/discriminatedUnionWithIndexSignature.types b/tests/baselines/reference/discriminatedUnionWithIndexSignature.types new file mode 100644 index 0000000000000..e288684a0b87a --- /dev/null +++ b/tests/baselines/reference/discriminatedUnionWithIndexSignature.types @@ -0,0 +1,55 @@ +//// [tests/cases/compiler/discriminatedUnionWithIndexSignature.ts] //// + +=== discriminatedUnionWithIndexSignature.ts === +export interface UnionAltA { + type?: 'text'; +>type : "text" | undefined +} + +export interface UnionAltB { + type?: 'image' | 'video' | 'document'; +>type : "image" | "video" | "document" | undefined +} + +export type ValueUnion = UnionAltA | UnionAltB; +>ValueUnion : UnionAltA | UnionAltB + +export type MapOrSingleton = +>MapOrSingleton : ValueUnion | { [key: string]: ValueUnion; } + + | { + [key: string]: ValueUnion; +>key : string + } + | ValueUnion; + +const withoutAsConst: MapOrSingleton = { +>withoutAsConst : MapOrSingleton +>{ 1: { type: 'text' /*as const*/, },} : { 1: { type: "text"; }; } + + 1: { +>1 : { type: "text"; } +>{ type: 'text' /*as const*/, } : { type: "text"; } + + type: 'text' /*as const*/, +>type : "text" +>'text' : "text" + + }, +}; + +const withAsConst: MapOrSingleton = { +>withAsConst : MapOrSingleton +>{ 1: { type: 'text' as const, },} : { 1: { type: "text"; }; } + + 1: { +>1 : { type: "text"; } +>{ type: 'text' as const, } : { type: "text"; } + + type: 'text' as const, +>type : "text" +>'text' as const : "text" +>'text' : "text" + + }, +}; diff --git a/tests/cases/compiler/discriminatedUnionWithIndexSignature.ts b/tests/cases/compiler/discriminatedUnionWithIndexSignature.ts new file mode 100644 index 0000000000000..95158f839438a --- /dev/null +++ b/tests/cases/compiler/discriminatedUnionWithIndexSignature.ts @@ -0,0 +1,29 @@ +// @strict: true + +export interface UnionAltA { + type?: 'text'; +} + +export interface UnionAltB { + type?: 'image' | 'video' | 'document'; +} + +export type ValueUnion = UnionAltA | UnionAltB; + +export type MapOrSingleton = + | { + [key: string]: ValueUnion; + } + | ValueUnion; + +const withoutAsConst: MapOrSingleton = { + 1: { + type: 'text' /*as const*/, + }, +}; + +const withAsConst: MapOrSingleton = { + 1: { + type: 'text' as const, + }, +}; \ No newline at end of file