Type Error when nesting union types in an intersected tagged-union type

Similar bug report: Tagged unions do not work when directly applied as an intersection type

Example of tagged unions:

As shown in the documentation:

type Ok<T> = { type: "ok", value: T }
type Err<E> = { type: "err", error: E }
type Result<T, E> = Ok<T> | Err<E>

if result.type == "ok" then
    -- result is known to be Ok<T>
    -- and attempting to index for error here will fail
    print(result.value)
elseif result.type == "err" then
    -- result is known to be Err<E>
    -- and attempting to index for value here will fail
    print(result.error)
end
Example of a tagged union and intersection:
--!strict

type TaggedUnionAndIntersection = ({
	tag: true,
	valueA: number,
} | {
	tag: false,
	valueB: string,
}) & {
	intersectionValue: string,
}

-- Works as expected
local variable: TaggedUnionAndIntersection = {
	tag = true,
	valueA = 0,
	intersectionValue = "",
}

-- Works as expected
local function func(parameter: TaggedUnionAndIntersection)
	if parameter.tag == true then
		print(parameter.valueA)
	else
		print(parameter.valueB)
	end
	print(parameter.intersectionValue)
end

Using a nested union type within a tagged union raises a Type Error with --!strict type inference (tested on old type solver and new type solver):

--!strict

type NestedUnionType1 = "Option1" | "Option2"
type NestedUnionType2 = "OptionA" | "OptionB"

type TaggedUnionAndIntersection = ({
	tag: "A",
	valueA: NestedUnionType1,
} | {
	tag: "B",
	valueB: NestedUnionType2,
}) & {
	intersectionValue: string,
}

-- Type Error
local variable: TaggedUnionAndIntersection = {
	tag = "A",
	valueA = "Option1",	-- `valueA` does not autofill
	intersectionValue = "",
}
Type Error: Expected this to be '({ tag: "A", valueA: "Option1" | "Option2" } | { tag: "B", valueB: "OptionA" | "OptionB" }) & { intersectionValue: string }' but got '{ intersectionValue: string, tag: "A", valueA: "Option1" }'; the 1st component of the intersection is `{ tag: "A", valueA: "Option1" | "Option2" } | { tag: "B", valueB: "OptionA" | "OptionB" }`, and `{ intersectionValue: string, tag: "A", valueA: "Option1" }` is not a subtype of `{ tag: "A", valueA: "Option1" | "Option2" } | { tag: "B", valueB: "OptionA" | "OptionB" }`

Functions do not raise a Type Error, but do not autofill the possible options for the nested union type

local function func(parameter: TaggedUnionAndIntersection)
	if parameter.tag == "A" then
		if parameter.valueA == "Option1" then -- `parameter.valueA` autofills, but `Option1` does not
			
		end
	else
		if parameter.valueB == "OptionA" then -- Same here

		end
	end
	
	print(parameter.intersectionValue)
end

Indirectly intersecting the types yields the same behavior:

--!strict

type UnionType1 = "Option1" | "Option2"
type UnionType2 = "OptionA" | "OptionB"

type TaggedUnion = {
	tag: "A",
	valueA: UnionType1,
} | {
	tag: "B",
	valueB: UnionType2,
}

type Intersection = {
	intersectionValue: string,
}

type TaggedUnionAndIntersection = TaggedUnion & Intersection

-- Same Type Error
local variable: TaggedUnionAndIntersection = {
	tag = "A",
	valueA = "Option1",
	intersectionValue = "",
}

Expected behavior

Using nested unions within a tagged union type should not raise a Type Error, and should autofill:

-- No Type Error
local variable: TaggedUnionAndIntersection = {
	tag = "A",
	valueA = "Option1",	-- `valueA` should autofill
	intersectionValue = "",
}

local function func(parameter: TaggedUnionAndIntersection)
	if parameter.tag == "A" then
		if parameter.valueA == "Option1" then -- `Option1` should autofill
			
		end
	else
		if parameter.valueB == "OptionA" then -- Same here
			
		end
	end
	
	print(parameter.intersectionValue)
end

Hello! Thank you for the report. Agreed that this is definitely a bug. We’re going to be working on some improvements to table type inference in the near future, so hang tight!

1 Like