From 7e4a96e2faa0899f34e9be80d5fe80a3eb9c9fe8 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Wed, 8 Jun 2022 11:30:28 -0700 Subject: [PATCH] Revise and simplify CFA for `typeof` check expressions (#49422) * Revise and simplify CFA for `typeof` check expressions * Accept new baselines * Add regression test * Slight change to preserve type when related in both directions * Add regression test * Explain reasons for exact sequence of type checks --- src/compiler/checker.ts | 240 +++++------------- tests/baselines/reference/mappedTypes4.types | 2 +- .../typeGuardOfFormTypeOfFunction.js | 23 ++ .../typeGuardOfFormTypeOfFunction.symbols | 31 +++ .../typeGuardOfFormTypeOfFunction.types | 37 +++ .../reference/typeGuardTypeOfUndefined.types | 6 +- .../reference/unknownControlFlow.types | 10 +- .../typeGuardOfFormTypeOfFunction.ts | 13 + 8 files changed, 178 insertions(+), 184 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index be2e6f9d8ab..2bea3c88536 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -144,17 +144,6 @@ namespace ts { AndFactsMask = All & ~OrFactsMask, } - const typeofEQFacts: ReadonlyESMap = new Map(getEntries({ - string: TypeFacts.TypeofEQString, - number: TypeFacts.TypeofEQNumber, - bigint: TypeFacts.TypeofEQBigInt, - boolean: TypeFacts.TypeofEQBoolean, - symbol: TypeFacts.TypeofEQSymbol, - undefined: TypeFacts.EQUndefined, - object: TypeFacts.TypeofEQObject, - function: TypeFacts.TypeofEQFunction - })); - const typeofNEFacts: ReadonlyESMap = new Map(getEntries({ string: TypeFacts.TypeofNEString, number: TypeFacts.TypeofNENumber, @@ -1031,14 +1020,6 @@ namespace ts { const diagnostics = createDiagnosticCollection(); const suggestionDiagnostics = createDiagnosticCollection(); - const typeofTypesByName: ReadonlyESMap = new Map(getEntries({ - string: stringType, - number: numberType, - bigint: bigintType, - boolean: booleanType, - symbol: esSymbolType, - undefined: undefinedType - })); const typeofType = createTypeofType(); let _jsxNamespace: __String; @@ -4181,7 +4162,7 @@ namespace ts { } function createTypeofType() { - return getUnionType(arrayFrom(typeofEQFacts.keys(), getStringLiteralType)); + return getUnionType(arrayFrom(typeofNEFacts.keys(), getStringLiteralType)); } function createTypeParameter(symbol?: Symbol) { @@ -23821,21 +23802,16 @@ namespace ts { return links.switchTypes; } - // Get the types from all cases in a switch on `typeof`. An - // `undefined` element denotes an explicit `default` clause. - function getSwitchClauseTypeOfWitnesses(switchStatement: SwitchStatement, retainDefault: false): string[]; - function getSwitchClauseTypeOfWitnesses(switchStatement: SwitchStatement, retainDefault: boolean): (string | undefined)[]; - function getSwitchClauseTypeOfWitnesses(switchStatement: SwitchStatement, retainDefault: boolean): (string | undefined)[] { + // Get the type names from all cases in a switch on `typeof`. The default clause and/or duplicate type names are + // represented as undefined. Return undefined if one or more case clause expressions are not string literals. + function getSwitchClauseTypeOfWitnesses(switchStatement: SwitchStatement): (string | undefined)[] | undefined { + if (some(switchStatement.caseBlock.clauses, clause => clause.kind === SyntaxKind.CaseClause && !isStringLiteralLike(clause.expression))) { + return undefined; + } const witnesses: (string | undefined)[] = []; for (const clause of switchStatement.caseBlock.clauses) { - if (clause.kind === SyntaxKind.CaseClause) { - if (isStringLiteralLike(clause.expression)) { - witnesses.push(clause.expression.text); - continue; - } - return emptyArray; - } - if (retainDefault) witnesses.push(/*explicitDefaultStatement*/ undefined); + const text = clause.kind === SyntaxKind.CaseClause ? (clause.expression as StringLiteralLike).text : undefined; + witnesses.push(text && !contains(witnesses, text) ? text : undefined); } return witnesses; } @@ -25093,14 +25069,9 @@ namespace ts { } return type; } - if (type.flags & TypeFlags.Any && literal.text === "function") { - return type; - } - const facts = assumeTrue ? - typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject : - typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject; - const impliedType = getImpliedTypeFromTypeofGuard(type, literal.text); - return getTypeWithFacts(assumeTrue && impliedType ? narrowTypeByImpliedType(type, impliedType) : type, facts); + return assumeTrue ? + narrowTypeByTypeName(type, literal.text) : + getTypeWithFacts(type, typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject); } function narrowTypeBySwitchOptionalChainContainment(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number, clauseCheck: (type: Type) => boolean) { @@ -25151,104 +25122,53 @@ namespace ts { return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]); } - function getImpliedTypeFromTypeofGuard(type: Type, text: string) { - switch (text) { - case "object": - return type.flags & TypeFlags.Any ? type : getUnionType([nullType, nonPrimitiveType]); - case "function": - return type.flags & TypeFlags.Any ? type : globalFunctionType; - default: - return typeofTypesByName.get(text); + function narrowTypeByTypeName(type: Type, typeName: string) { + switch (typeName) { + case "string": return narrowTypeByTypeFacts(type, stringType, TypeFacts.TypeofEQString); + case "number": return narrowTypeByTypeFacts(type, numberType, TypeFacts.TypeofEQNumber); + case "bigint": return narrowTypeByTypeFacts(type, bigintType, TypeFacts.TypeofEQBigInt); + case "boolean": return narrowTypeByTypeFacts(type, booleanType, TypeFacts.TypeofEQBoolean); + case "symbol": return narrowTypeByTypeFacts(type, esSymbolType, TypeFacts.TypeofEQSymbol); + case "object": return type.flags & TypeFlags.Any ? type : getUnionType([narrowTypeByTypeFacts(type, nonPrimitiveType, TypeFacts.TypeofEQObject), narrowTypeByTypeFacts(type, nullType, TypeFacts.EQNull)]); + case "function": return type.flags & TypeFlags.Any ? type : narrowTypeByTypeFacts(type, globalFunctionType, TypeFacts.TypeofEQFunction); + case "undefined": return narrowTypeByTypeFacts(type, undefinedType, TypeFacts.EQUndefined); } + return narrowTypeByTypeFacts(type, nonPrimitiveType, TypeFacts.TypeofEQHostObject); } - // When narrowing a union type by a `typeof` guard using type-facts alone, constituent types that are - // super-types of the implied guard will be retained in the final type: this is because type-facts only - // filter. Instead, we would like to replace those union constituents with the more precise type implied by - // the guard. For example: narrowing `{} | undefined` by `"boolean"` should produce the type `boolean`, not - // the filtered type `{}`. For this reason we narrow constituents of the union individually, in addition to - // filtering by type-facts. - function narrowTypeByImpliedType(type: Type, candidate: Type) { - if (type.flags & TypeFlags.AnyOrUnknown) { - return candidate; - } - return mapType(type, t => { - if (isTypeRelatedTo(t, candidate, strictSubtypeRelation)) { - return t; - } - return mapType(candidate, c => { - if (!areTypesComparable(t, c)) { - return neverType; - } - if (c.flags & TypeFlags.Primitive && t.flags & TypeFlags.Object && !isEmptyAnonymousObjectType(t)) { - return isTypeSubtypeOf(c, t) ? c : neverType; - } - if (c === globalFunctionType && isTypeSubtypeOf(c, t)) { - return c; - } - return getIntersectionType([t, c]); - }); - }); + function narrowTypeByTypeFacts(type: Type, impliedType: Type, facts: TypeFacts) { + return mapType(type, t => + // We first check if a constituent is a subtype of the implied type. If so, we either keep or eliminate + // the constituent based on its type facts. We use the strict subtype relation because it treats `object` + // as a subtype of `{}`, and we need the type facts check because function types are subtypes of `object`, + // but are classified as "function" according to `typeof`. + isTypeRelatedTo(t, impliedType, strictSubtypeRelation) ? getTypeFacts(t) & facts ? t : neverType : + // We next check if the consituent is a supertype of the implied type. If so, we substitute the implied + // type. This handles top types like `unknown` and `{}`, and supertypes like `{ toString(): string }`. + isTypeSubtypeOf(impliedType, t) ? impliedType : + // Neither the constituent nor the implied type is a subtype of the other, however their domains may still + // overlap. For example, an unconstrained type parameter and type `string`. If the type facts indicate + // possible overlap, we form an intersection. Otherwise, we eliminate the constituent. + getTypeFacts(t) & facts ? getIntersectionType([t, impliedType]) : + neverType); } function narrowBySwitchOnTypeOf(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): Type { - const switchWitnesses = getSwitchClauseTypeOfWitnesses(switchStatement, /*retainDefault*/ true); - if (!switchWitnesses.length) { + const witnesses = getSwitchClauseTypeOfWitnesses(switchStatement); + if (!witnesses) { return type; } - // Equal start and end denotes implicit fallthrough; undefined marks explicit default clause - const defaultCaseLocation = findIndex(switchWitnesses, elem => elem === undefined); - const hasDefaultClause = clauseStart === clauseEnd || (defaultCaseLocation >= clauseStart && defaultCaseLocation < clauseEnd); - let clauseWitnesses: string[]; - let switchFacts: TypeFacts; - if (defaultCaseLocation > -1) { - // We no longer need the undefined denoting an explicit default case. Remove the undefined and - // fix-up clauseStart and clauseEnd. This means that we don't have to worry about undefined in the - // witness array. - const witnesses = switchWitnesses.filter(witness => witness !== undefined) as string[]; - // The adjusted clause start and end after removing the `default` statement. - const fixedClauseStart = defaultCaseLocation < clauseStart ? clauseStart - 1 : clauseStart; - const fixedClauseEnd = defaultCaseLocation < clauseEnd ? clauseEnd - 1 : clauseEnd; - clauseWitnesses = witnesses.slice(fixedClauseStart, fixedClauseEnd); - switchFacts = getFactsFromTypeofSwitch(fixedClauseStart, fixedClauseEnd, witnesses, hasDefaultClause); - } - else { - clauseWitnesses = switchWitnesses.slice(clauseStart, clauseEnd) as string[]; - switchFacts = getFactsFromTypeofSwitch(clauseStart, clauseEnd, switchWitnesses as string[], hasDefaultClause); - } + // Equal start and end denotes implicit fallthrough; undefined marks explicit default clause. + const defaultIndex = findIndex(switchStatement.caseBlock.clauses, clause => clause.kind === SyntaxKind.DefaultClause); + const hasDefaultClause = clauseStart === clauseEnd || (defaultIndex >= clauseStart && defaultIndex < clauseEnd); if (hasDefaultClause) { - return filterType(type, t => (getTypeFacts(t) & switchFacts) === switchFacts); + // In the default clause we filter constituents down to those that are not-equal to all handled cases. + const notEqualFacts = getNotEqualFactsFromTypeofSwitch(clauseStart, clauseEnd, witnesses); + return filterType(type, t => (getTypeFacts(t) & notEqualFacts) === notEqualFacts); } - /* - The implied type is the raw type suggested by a - value being caught in this clause. - - When the clause contains a default case we ignore - the implied type and try to narrow using any facts - we can learn: see `switchFacts`. - - Example: - switch (typeof x) { - case 'number': - case 'string': break; - default: break; - case 'number': - case 'boolean': break - } - - In the first clause (case `number` and `string`) the - implied type is number | string. - - In the default clause we de not compute an implied type. - - In the third clause (case `number` and `boolean`) - the naive implied type is number | boolean, however - we use the type facts to narrow the implied type to - boolean. We know that number cannot be selected - because it is caught in the first clause. - */ - const impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => getImpliedTypeFromTypeofGuard(type, text) || type)), switchFacts); - return getTypeWithFacts(narrowTypeByImpliedType(type, impliedType), switchFacts); + // In the non-default cause we create a union of the type narrowed by each of the listed cases. + const clauseWitnesses = witnesses.slice(clauseStart, clauseEnd); + return getUnionType(map(clauseWitnesses, text => text ? narrowTypeByTypeName(type, text) : neverType)); } function isMatchingConstructorReference(expr: Expression) { @@ -32810,45 +32730,12 @@ namespace ts { : Diagnostics.Type_of_yield_operand_in_an_async_generator_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member); } - /** - * Collect the TypeFacts learned from a typeof switch with - * total clauses `witnesses`, and the active clause ranging - * from `start` to `end`. Parameter `hasDefault` denotes - * whether the active clause contains a default clause. - */ - function getFactsFromTypeofSwitch(start: number, end: number, witnesses: string[], hasDefault: boolean): TypeFacts { + // Return the combined not-equal type facts for all cases except those between the start and end indices. + function getNotEqualFactsFromTypeofSwitch(start: number, end: number, witnesses: (string | undefined)[]): TypeFacts { let facts: TypeFacts = TypeFacts.None; - // When in the default we only collect inequality facts - // because default is 'in theory' a set of infinite - // equalities. - if (hasDefault) { - // Value is not equal to any types after the active clause. - for (let i = end; i < witnesses.length; i++) { - facts |= typeofNEFacts.get(witnesses[i]) || TypeFacts.TypeofNEHostObject; - } - // Remove inequalities for types that appear in the - // active clause because they appear before other - // types collected so far. - for (let i = start; i < end; i++) { - facts &= ~(typeofNEFacts.get(witnesses[i]) || 0); - } - // Add inequalities for types before the active clause unconditionally. - for (let i = 0; i < start; i++) { - facts |= typeofNEFacts.get(witnesses[i]) || TypeFacts.TypeofNEHostObject; - } - } - // When in an active clause without default the set of - // equalities is finite. - else { - // Add equalities for all types in the active clause. - for (let i = start; i < end; i++) { - facts |= typeofEQFacts.get(witnesses[i]) || TypeFacts.TypeofEQHostObject; - } - // Remove equalities for types that appear before the - // active clause. - for (let i = 0; i < start; i++) { - facts &= ~(typeofEQFacts.get(witnesses[i]) || 0); - } + for (let i = 0; i < witnesses.length; i++) { + const witness = i < start || i >= end ? witnesses[i] : undefined; + facts |= witness !== undefined ? typeofNEFacts.get(witness) || TypeFacts.TypeofNEHostObject : 0; } return facts; } @@ -32860,16 +32747,19 @@ namespace ts { function computeExhaustiveSwitchStatement(node: SwitchStatement): boolean { if (node.expression.kind === SyntaxKind.TypeOfExpression) { - const operandType = getTypeOfExpression((node.expression as TypeOfExpression).expression); - const witnesses = getSwitchClauseTypeOfWitnesses(node, /*retainDefault*/ false); - // notEqualFacts states that the type of the switched value is not equal to every type in the switch. - const notEqualFacts = getFactsFromTypeofSwitch(0, 0, witnesses, /*hasDefault*/ true); - const type = getBaseConstraintOfType(operandType) || operandType; - // Take any/unknown as a special condition. Or maybe we could change `type` to a union containing all primitive types. - if (type.flags & TypeFlags.AnyOrUnknown) { + const witnesses = getSwitchClauseTypeOfWitnesses(node); + if (!witnesses) { + return false; + } + const operandConstraint = getBaseConstraintOrType(getTypeOfExpression((node.expression as TypeOfExpression).expression)); + // Get the not-equal flags for all handled cases. + const notEqualFacts = getNotEqualFactsFromTypeofSwitch(0, 0, witnesses); + if (operandConstraint.flags & TypeFlags.AnyOrUnknown) { + // We special case the top types to be exhaustive when all cases are handled. return (TypeFacts.AllTypeofNE & notEqualFacts) === TypeFacts.AllTypeofNE; } - return !!(filterType(type, t => (getTypeFacts(t) & notEqualFacts) === notEqualFacts).flags & TypeFlags.Never); + // A missing not-equal flag indicates that the type wasn't handled by some case. + return !someType(operandConstraint, t => (getTypeFacts(t) & notEqualFacts) === notEqualFacts); } const type = getTypeOfExpression(node.expression); if (!isLiteralType(type)) { diff --git a/tests/baselines/reference/mappedTypes4.types b/tests/baselines/reference/mappedTypes4.types index f96bf1c5e8f..b51aefa9cce 100644 --- a/tests/baselines/reference/mappedTypes4.types +++ b/tests/baselines/reference/mappedTypes4.types @@ -27,7 +27,7 @@ function boxify(obj: T): Boxified { for (let k in obj) { >k : Extract ->obj : (T & null) | (T & object) +>obj : (T & object) | (T & null) result[k] = { value: obj[k] }; >result[k] = { value: obj[k] } : { value: (T & object)[Extract]; } diff --git a/tests/baselines/reference/typeGuardOfFormTypeOfFunction.js b/tests/baselines/reference/typeGuardOfFormTypeOfFunction.js index 2dfff84c36b..2e4ffe7c660 100644 --- a/tests/baselines/reference/typeGuardOfFormTypeOfFunction.js +++ b/tests/baselines/reference/typeGuardOfFormTypeOfFunction.js @@ -71,6 +71,19 @@ function f100(obj: T, keys: K[]) : void { item.call(obj); } } + +// Repro from #49316 + +function configureStore(reducer: (() => void) | Record void>) { + let rootReducer: () => void; + if (typeof reducer === 'function') { + rootReducer = reducer; + } +} + +function f101(x: string | Record) { + return typeof x === "object" && x.anything; +} //// [typeGuardOfFormTypeOfFunction.js] @@ -137,3 +150,13 @@ function f100(obj, keys) { item.call(obj); } } +// Repro from #49316 +function configureStore(reducer) { + var rootReducer; + if (typeof reducer === 'function') { + rootReducer = reducer; + } +} +function f101(x) { + return typeof x === "object" && x.anything; +} diff --git a/tests/baselines/reference/typeGuardOfFormTypeOfFunction.symbols b/tests/baselines/reference/typeGuardOfFormTypeOfFunction.symbols index fa9e9ff515b..f3032a964fc 100644 --- a/tests/baselines/reference/typeGuardOfFormTypeOfFunction.symbols +++ b/tests/baselines/reference/typeGuardOfFormTypeOfFunction.symbols @@ -157,3 +157,34 @@ function f100(obj: T, keys: K[]) : void { } } +// Repro from #49316 + +function configureStore(reducer: (() => void) | Record void>) { +>configureStore : Symbol(configureStore, Decl(typeGuardOfFormTypeOfFunction.ts, 71, 1)) +>S : Symbol(S, Decl(typeGuardOfFormTypeOfFunction.ts, 75, 24)) +>reducer : Symbol(reducer, Decl(typeGuardOfFormTypeOfFunction.ts, 75, 42)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) +>S : Symbol(S, Decl(typeGuardOfFormTypeOfFunction.ts, 75, 24)) + + let rootReducer: () => void; +>rootReducer : Symbol(rootReducer, Decl(typeGuardOfFormTypeOfFunction.ts, 76, 7)) + + if (typeof reducer === 'function') { +>reducer : Symbol(reducer, Decl(typeGuardOfFormTypeOfFunction.ts, 75, 42)) + + rootReducer = reducer; +>rootReducer : Symbol(rootReducer, Decl(typeGuardOfFormTypeOfFunction.ts, 76, 7)) +>reducer : Symbol(reducer, Decl(typeGuardOfFormTypeOfFunction.ts, 75, 42)) + } +} + +function f101(x: string | Record) { +>f101 : Symbol(f101, Decl(typeGuardOfFormTypeOfFunction.ts, 80, 1)) +>x : Symbol(x, Decl(typeGuardOfFormTypeOfFunction.ts, 82, 14)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) + + return typeof x === "object" && x.anything; +>x : Symbol(x, Decl(typeGuardOfFormTypeOfFunction.ts, 82, 14)) +>x : Symbol(x, Decl(typeGuardOfFormTypeOfFunction.ts, 82, 14)) +} + diff --git a/tests/baselines/reference/typeGuardOfFormTypeOfFunction.types b/tests/baselines/reference/typeGuardOfFormTypeOfFunction.types index fbcbd82cf16..8166bc9e78b 100644 --- a/tests/baselines/reference/typeGuardOfFormTypeOfFunction.types +++ b/tests/baselines/reference/typeGuardOfFormTypeOfFunction.types @@ -182,3 +182,40 @@ function f100(obj: T, keys: K[]) : void { } } +// Repro from #49316 + +function configureStore(reducer: (() => void) | Record void>) { +>configureStore : (reducer: (() => void) | Record void>) => void +>reducer : Record void> | (() => void) + + let rootReducer: () => void; +>rootReducer : () => void + + if (typeof reducer === 'function') { +>typeof reducer === 'function' : boolean +>typeof reducer : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>reducer : Record void> | (() => void) +>'function' : "function" + + rootReducer = reducer; +>rootReducer = reducer : () => void +>rootReducer : () => void +>reducer : () => void + } +} + +function f101(x: string | Record) { +>f101 : (x: string | Record) => any +>x : string | Record + + return typeof x === "object" && x.anything; +>typeof x === "object" && x.anything : any +>typeof x === "object" : boolean +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : string | Record +>"object" : "object" +>x.anything : any +>x : Record +>anything : any +} + diff --git a/tests/baselines/reference/typeGuardTypeOfUndefined.types b/tests/baselines/reference/typeGuardTypeOfUndefined.types index ffc82e57e66..79f87e101ca 100644 --- a/tests/baselines/reference/typeGuardTypeOfUndefined.types +++ b/tests/baselines/reference/typeGuardTypeOfUndefined.types @@ -242,7 +242,7 @@ function test9(a: boolean | number) { } else { a; ->a : never +>a : undefined } } @@ -259,7 +259,7 @@ function test10(a: boolean | number) { if (typeof a === "boolean") { >typeof a === "boolean" : boolean >typeof a : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" ->a : never +>a : undefined >"boolean" : "boolean" a; @@ -267,7 +267,7 @@ function test10(a: boolean | number) { } else { a; ->a : never +>a : undefined } } else { diff --git a/tests/baselines/reference/unknownControlFlow.types b/tests/baselines/reference/unknownControlFlow.types index 32b0c7293c1..cfd88dbffc8 100644 --- a/tests/baselines/reference/unknownControlFlow.types +++ b/tests/baselines/reference/unknownControlFlow.types @@ -410,7 +410,7 @@ function f31(x: T) { >"object" : "object" x; // T & object | T & null ->x : (T & null) | (T & object) +>x : (T & object) | (T & null) } if (x && typeof x === "object") { >x && typeof x === "object" : boolean @@ -424,12 +424,12 @@ function f31(x: T) { >x : T & object } if (typeof x === "object" && x) { ->typeof x === "object" && x : false | (T & null) | (T & object) +>typeof x === "object" && x : false | (T & object) | (T & null) >typeof x === "object" : boolean >typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" >x : T >"object" : "object" ->x : (T & null) | (T & object) +>x : (T & object) | (T & null) x; // T & object >x : T & object @@ -650,9 +650,9 @@ function deepEquals(a: T, b: T): boolean { >b : T >'object' : "object" >!a : boolean ->a : (T & null) | (T & object) +>a : (T & object) | (T & null) >!b : boolean ->b : (T & null) | (T & object) +>b : (T & object) | (T & null) return false; >false : false diff --git a/tests/cases/conformance/expressions/typeGuards/typeGuardOfFormTypeOfFunction.ts b/tests/cases/conformance/expressions/typeGuards/typeGuardOfFormTypeOfFunction.ts index cbacf60ce68..8008f74b848 100644 --- a/tests/cases/conformance/expressions/typeGuards/typeGuardOfFormTypeOfFunction.ts +++ b/tests/cases/conformance/expressions/typeGuards/typeGuardOfFormTypeOfFunction.ts @@ -71,3 +71,16 @@ function f100(obj: T, keys: K[]) : void { item.call(obj); } } + +// Repro from #49316 + +function configureStore(reducer: (() => void) | Record void>) { + let rootReducer: () => void; + if (typeof reducer === 'function') { + rootReducer = reducer; + } +} + +function f101(x: string | Record) { + return typeof x === "object" && x.anything; +}