Skip to content

Empty Object type hides other types when mixing them with statements or expressions #49952

Closed as not planned

Description

@pyBlob

Bug Report

🔎 Search Terms

  • object hides string, inferred return type

🕗 Version & Regression Information

  • This is the behaviour in every version I tried. The documentation does not say what should happen when {} is combined with other types during control flow inference.

⏯ Playground Link

Playground link with relevant code

💻 Code

Definitions:

// ok: "bob"
const bob: "bob" = "bob"

// ok: {}
const empty: {} = {}

// ok: {} | "bob"
type BO = "bob" | {}

// ok: {} | "bob"
type TBO = typeof bob | typeof empty

// ok: "bob"
type XTBO = Extract<TBO, string>

Using Array:

// bad: {}[], should be: ({} | "bob")[]
const array = [bob, empty]

// bad: {}, should be: {} | "bob"
type A = typeof array[number]

// bad: never, should be: "bob"
type XA = Extract<typeof array[number], string>

// bad: {}[], should be: (Date | {})[]
// Bad, because you can still recover Date: (a0[index] instanceof Date) implies that it really is a Date
const a0 = [new Date(), empty]

// ok: ({} | Date)[]
type A0 = (Date | {})[]

// bad: {}[], should be: ({} | 42)[], or ({} | number)[]
const a1 = [42, empty]

// ok: ({} | 42)[]
type A1 = ({} | 42)[]

// ok: {}[]
// Ok, because ("typescript" in a2[index]) does not imply that (a2[index].typescript === "rocks")
const a2 = [{ typescript: "rocks" }, empty]

// ok: ({} | { typescript: "rocks" })[]
type A2 = ({ typescript: "rocks" } | {})[]

Using if:

function ifAction(names: boolean) {
    if (names) {
        return bob
    } else {
        return {} as const
    }
}

// bad: {}, should be: {} | "bob"
type I = ReturnType<typeof ifAction>

// bad: never, should be: "bob"
type XI = Extract<ReturnType<typeof ifAction>, string>

Using switch-case + call to function (same result when directly return the constants)

export type Action = Names | Label

export type Names = "Names"

export type Label = "Label"

function action(action: Action) {
	switch (action) {
		case "Names":
			return actionNames(action)
		case "Label":
			return actionLabel(action)
	}
}

function actionNames(action: Names) {
	return bob
}

function actionLabel(action: Label) {
	return empty
}

// ok: "bob"
type N = ReturnType<typeof actionNames>

// ok: {}
type L = ReturnType<typeof actionLabel>

// bad: {}, should be: {} | "bob"
type R = ReturnType<typeof action>

// bad: never, should be: "bob"
type XS = Extract<ReturnType<typeof action>, string>

Actual/Expected behavior

It should not hide the string-type, see comments in code

Activity

  1. MartinJohns commented on Jul 19, 2022

    @MartinJohns
    Contributor

    This is the expected subtype reduction behavior. Note that {} does not mean "empty object type". It means any type except null and undefined.

  2. pyBlob commented on Jul 20, 2022

    @pyBlob
    ContributorAuthor

    Right, as everything except null and undefined is an object in javascript, this actually makes sense.

    What doesn't make so much sense, is that typescript does not always do the reduction, e.g. when explicitly taking the union of both types like in the TBO type above.

    As a second thought, the same reasoning should apply { toString(): string }, which it does most of the time. Inconsistencies only get worse/change when adding generics into the mix (see type-hints / .d.ts outputs):
    Playground Link

  3. RyanCavanaugh commented on Jul 20, 2022

    @RyanCavanaugh
    Member

    Subtype reduction happens "sometimes" where "sometimes" is largely determined by performance constraints. It's never wrong to subtype-reduce so any observed behavior is from an equally-valid set of behaviors.

  4. pyBlob commented on Jul 20, 2022

    @pyBlob
    ContributorAuthor

    It's never wrong to subtype-reduce so any observed behavior is from an equally-valid set of behaviors.

    I agree. I tried expressions [a, b] and observed the inferred types:

    • Only those expressions where the type of one variable is a proper subtype of the other have been reduced to a single type. After looking at those cases, this really prevents errors.
    • For other cases, the item type of the array was the union of both types.
      • When there is a discriminant property, you should be fine.
      • When both types only overlap without a discriminant property. By definition, it is only safe to use common properties. There will be dragons when you use conditional types or control flow analysis on properties defined only in one of the types. It would definitely help, if the effective type would be more general -> only allow to observe properties common to both types (like with member access).
  5. github-actions commented on Jun 8, 2023

    @github-actions

    This issue has been marked as 'Not a Defect' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  6. locked as resolved and limited conversation to collaborators on Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Not a DefectThis behavior is one of several equally-correct options

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions