An “Argument count mismatch” Type Error is raised when using Luau’s new type solver when string.format does not receive a formatstring directly, even if a valid format string is passed via a variable.
--!strict
local str = "%s"
string.format(str, "Hello world!")
Beta Features:
New Luau type solver
This issue does not occur on the old type solver.
Expected behavior
A Type Error should not be raised if formatstring is a variable that is a valid format string or if the variable string’s content is unknown, such as in the context of a function that calls string.format with an argument like in the below example:
local function formatString(formatstring: string, ...: any)
return string.format(formatstring, ...)
end
Specifying the type as a string does not fix the issue, string.format checks more than just if it’s a string — it checks if it’s a valid format string — which seems to be partially faulty in the new type solver.
Thank you for the report! This change is largely by-design, though the error message can definitely be improved and I’ll look into doing so. If you look at the behavior in the old solver, you can see why this has changed:
local str = "%s"
string.format(str, 5) -- no type error
string.format("%s", 5) -- type error
This is a problem because the typechecker is admitting an ill-typed argument for that formatter. With the New Type Solver, we’re moving towards a vision of Luau that is explicitly type safe, and that means closing out longstanding holes in the type system like this. This is similar to the behavior that already happens for require when used dynamically. We give you an error indicating that we can’t do a useful static analysis of your program because of that, and that’s just as true of string.format if we are unable to determine what format specifier is being used.
That being said, there are two changes that I think we can reasonably make here though to make things better, besides adjusting the error message. First, we can let you opt out of typechecking for string.format more easily (you can currently do so with (string.format :: any)(str, 5)) by admitting all calls if the format string is typed as any. This means you’ll be able to do string.format(str :: any, ...) as an alternative for an opt-in unchecked string.format call. The other thing we can do is allow string.format’s magic function (the thing that gives it its special typechecking for format specifiers) to look at the type when there’s no string literal there itself. This would mean that if you had a singleton-typed string, we’d be able to retain the safe check for string.format, e.g. the following code could behave safely:
local str: "%s" = "%s"
string.format(str, "foo") -- no type error
string.format(str, 5) -- type error