Generic intersection only enforced one side of table types

--!strict

type Predicate<T> = {
	Type: () -> T	
}

type function Interface(shape)
	local out = types.newtable()

	for key, prop in shape:properties() do
		out:setproperty(key, prop.read:readproperty(types.singleton("Type")):returns().head[1])
	end

	return out
end

local function makeStringPredicate(): Predicate<string>
	return nil :: never
end

local function makeInterfacePredicate<T>(tbl: T): Predicate<Interface<T>>
	return nil :: never
end

local function intersect<A, B>(a: Predicate<A>, b: Predicate<B>): Predicate<A & B>
	return nil :: never
end

local a = makeInterfacePredicate({ a = makeStringPredicate() })
local b = makeInterfacePredicate({ b = makeStringPredicate() })

local intersection = intersect(a, b)

type IntersectionType = typeof(intersection.Type())

local bad: IntersectionType = {
	a = 32, -- This gives me a warning, which is correct!
	b = 91, -- This doesn't? But it should be the same as the one above
}

This appears to be a bug, but I wanted to confirm that I’m not misunderstanding something in my code.

I’m working on a runtime type-checking library and ran into an issue with intersections. Individually, the interface predicates seem to produce the correct types, but when I combine them through a generic, one side of the resulting table loses it’s property type checking.

In this example, I would expect both a and b to be checked as a string. Instead, only one side is enforced, while the other seems to get turned into unknown.

image

this is not a runtime bug, it’s a Roblox Luau type inference issue.

You need to force Luau to infer literal property types, rather than “widened generics”.

local a: Predicate<{ a: string }> = makeInterfacePredicate({ a = makeStringPredicate() })
local b: Predicate<{ b: string }> = makeInterfacePredicate({ b = makeStringPredicate() })

This gives the compiler a concrete type