How to imply a valid character model using Luau?

I’ve been using this method for a very long time. I want to migrate it to be Luau compatible. My methods, CharValid and CharAlive, should guarantee the existence of certain properties similar to assert() or if statements.

--!strict
local module = {}

function module:CharValid(char: Instance?)
	return char and char.Parent and char:IsA("Model") and char.PrimaryPart
end

function module:CharAlive(char: Instance?)
	return module:CharValid(char) and char:FindFirstChild("Humanoid") and char.Humanoid.Health > 0
end

-- use case: kill brick
local function onTouch(hit: BasePart)
	if module:CharAlive(hit.Parent) then -- CharAlive implies hit.Parent, humanoid, and PrimaryPart
		hit.Parent.Humanoid:TakeDamage(100)
	end
end

You can see it’s not very happy with me directly accessing hit.Parent.Humanoid even though the :CharAlive function guarantees it won’t error.

-- use case: kill brick w/o :CharAlive
local function onTouch(hit: BasePart)
	if hit.Parent and hit.Parent:IsA("Model") and hit.Parent.PrimaryPart then -- CharAlive implies hit.Parent, humanoid, and PrimaryPart then
		local humanoid = hit.Parent:FindFirstChild("Humanoid") :: Humanoid
		if humanoid and humanoid.Health > 0 then
			humanoid:TakeDamage(100)
		end
	end
end

Here’s how I did it to make the Luau linter happy. It defeats the purpose, since there’s now 2 separate if statements with a :FindFirstChild cast. I want to turn all of that mess into a single function check similar to :CharAlive that’ll keep the Luau linter happy. How can I do so?

As far as I know, luau doesn’t have an in-built type for player characters. The only way that comes into my mind is to create your custom type and use it instead like this:

export type Character = {
	PrimaryPart:Instance?,
	Humanoid:Humanoid,
}

Do you mean to do the check then cast it? I’m still getting a warning that the casting isn’t compatible.

export type Character = {
	PrimaryPart:Instance,
	Humanoid:Humanoid,
}

function module:CharValid(char: Instance?)
	return char and char.Parent and char:IsA("Model") and char.PrimaryPart
end

function module:CharAlive(char: Instance?)
	return module:CharValid(char) and char:FindFirstChild("Humanoid") and char.Humanoid.Health > 0
end

-- use case: kill brick
local function onTouch(hit: BasePart)
	if module:CharAlive(hit.Parent) then -- CharAlive implies hit.Parent, humanoid, and PrimaryPart
		local char = hit.Parent :: Character
		hit.Parent.Humanoid:TakeDamage(100)
	end
end
1 Like

I usually just get around this by validating a local variable using :FindFirstChildOfClass()

function charAlive(char: Model): boolean
	local humanoid = char:FindFirstChildOfClass("Humanoid")
	if not humanoid then return false end
	--if statement works and the type check will assume it exists

	--autocomplete works
	return humanoid.Health > 0 and humanoid:GetState() ~= Enum.HumanoidStateType.Dead
end

the type checker doesnt assume children exist for any given instance, only for its own properties (given that the instance exists)

it also doesn’t like like single line assertions that much.

The only real way to access it with the dot is by extending the type

type Character = Model & { --extends the type
	Humanoid: Humanoid,
	PrimaryPart: Part
}
1 Like