Luau typechecking failing with intersections on complex types

Sometimes when using complex luau types that are built up of many sub-types and intersections the script editor will highlight the problem variable as being unable to convert/cast etc.

This could result from a misunderstanding on my end, but I’ve shared my example case w/ a few people and they too can’t really figure out what’s going on.

For example, say my type definitions are like so:

--!strict

type PetsOriginMandatory = {
	year: number,
}

type PetsOriginOptional = {
	example: string?,
}

type PetsOrigin = PetsOriginMandatory & PetsOriginOptional

type PetsMandatory = {
	origin: PetsOrigin,
	model: string,
}

type PetsOptional = {
	is_egg: boolean?,
}

type Pets = PetsMandatory & PetsOptional

When I try to use these definitions the script editor yells at me:

local function methodTakesPets(pets: { [string]: Pets })
	
end

-- incorrect failure and unhelpful error:
methodTakesPets({
	["cat"] = {
		model = "Cat",

		origin = {
			year = 2019,
			example = "test",
		},

		is_egg = false,
	},
})

-- incorrect failure and unhelpful error:
local cat = {
	model = "Cat",

	origin = {
		year = 2019,
		example = "test",
	},

	is_egg = false,
} :: Pets

-- incorrect failure and helpful but incorrect error:
local cat: Pets = {
	model = "Cat",

	origin = {
		year = 2019,
		example = "test",
	},

	is_egg = false,
}

Also, in the above when I say “unhelpful error” I mean that when hovering the error provided by luau doesn’t actually give me any useful information that I could use to fix the perceived issue.

For example:

As far as I can tell the issue seems to be that the origin table can’t seem to match up with the origin type because of something to do with intersections.

I can get it to work if:

  • the origin type is combined manually instead of using intersection types
  • or the Pets type is combined manually instead of using intersection types
  • or the origin table is typecasted with :: PetsOrigin

However, I’m not super keen on using typecasting if it’s not going to work properly in all circumstances.

2 Likes

This needs read/write properties (see the RFC) to be able to decide whether this subtyping is valid. We’re working on the new type checking architecture, and that new architecture plus readonly properties will help to fix this without the need for casts.

1 Like

Hi there, thank you for the report! I just wanted to circle back and add that the situation is partially improved today in the new type solver, but the quality of errors here is still quite bad, and we’d like to fix those as well. The remaining avenue for improvements from my perspective seems to be improvements to our bidirectional type inference logic since it appears like the failures are rooted at the optional properties not being assignable. As Apakovtac was implying in the past, there’s an actual underlying safety reason for the invariant mismatch errors in general, but because all of this code deals with table literals in particular, we should nevertheless be able to handle it even without any read/write annotations.

Hello! Checking in to say that this looks fixed in the new solver, the following snippet:

--!strict

type PetsOriginMandatory = {
	year: number,
}

type PetsOriginOptional = {
	example: string?,
}

type PetsOrigin = PetsOriginMandatory & PetsOriginOptional

type PetsMandatory = {
	origin: PetsOrigin,
	model: string,
}

type PetsOptional = {
	is_egg: boolean?,
}

type Pets = PetsMandatory & PetsOptional

local function methodTakesPets(pets: { [string]: Pets })

end

methodTakesPets({
	["cat"] = {
		model = "Cat",

		origin = {
			year = 2019,
			example = "test",
		},

		is_egg = false,
	},
})

local cat: Pets = {
	model = "Cat",

	origin = {
		year = 2019,
		example = "test",
	},

	is_egg = false,
}

Has no errors.