If you were seeking just a specificity, it would likely manifest as:
type UniqueFn = (...args: any[]) => boolean;
or potentially the more secure option:
type UniqueFn = (...args: never[]) => boolean;
which will validate any function that produces a boolean
result.
However, there's a caveat: when you define a variable with this type, the compiler will unfortunately discard any specific information related to the number and types of parameters, only acknowledging that the function returns a boolean
. The details are lost in translation.
The version with any[]
will allow you to invoke the function with correct arguments:
type UniqueFn = (...args: any[]) => boolean;
const g: UniqueFn = (x: string) => x.toUpperCase() === x.toLowerCase();
g("okay");
Yet, it will also permit calling the function with incorrect parameters:
try {
g(123); // unexpected behavior
} catch (err) {
console.log(err) // RUNTIME ERROR 💥 x.toUppercase is not a function
}
On the flip side, the version with never[]
is so strict that it won't even let you call the function with the right parameters:
type UniqueFn = (...args: never[]) => boolean;
const g: UniqueFn = (x: string) => x.toUpperCase() === x.toLowerCase();
g("okay"); // error, not permitted!
The issue here lies not so much within the UniqueFn
type, but with the instances of that type.
My recommendation would be to swap out type annotations for the satisfies
operator. When you declare const varName: Type = value
, varName
usually knows solely about Type
and not potentially more precise typeof value
. On the contrary, if you define
const varName = value satisfies Type
, the compiler ensures that
value
satisfies Type
without broadening it. And
varName
's type remains
typeof value
, possibly more detailed than
Type
.
For the earlier sample code, this translates to:
const h = (
(x: string) => x.toUpperCase() === x.toLowerCase()
) satisfies UniqueFn; // okay
h("okay"); // okay
h(123); // compilation error
Observe how h
is verified as satisfying UniqueFn
, yet the compiler retains its type as (x: string) => boolean
, allowing h("okay")
while flagging h(123)
.
For your provided code snippet, we can follow a similar approach:
interface ValidationCheck {
name: string
verifier: UniqueFn
}
const validationChecks = [
{
name: 'required',
verifier: (x?: string) => {
return x != null
}
},
{
name: 'greaterThan',
verifier: (entry: number, maximum: number) => {
return entry > maximum
}
},
// Uncommenting below line reveals an error
// { name: 'mistake', verifier: (a: string, b: number) => "uh-oh" }
] as const satisfies readonly ValidationCheck[];
Here, we utilize a const
assertion to prompt the compiler to preserve the exact literal types of the name
properties within the array elements. Instead of widening validationChecks
to ValidationCheck[]
and losing specificity, we simply use satisfies
to ensure its compatibility.
If an error is made in defining validationChecks
, it results in an error on satisfies
:
const invalidChecks = [
{
name: 'required',
verifier: (x?: string) => {
return x != null
}
},
{
name: 'greaterThan',
verifier: (entry: number, maximum: number) => {
return entry > maximum
}
},
{ name: 'mistake', verifier: (a: string, b: number) => "uh-oh" }
] as const satisfies readonly ValidationCheck[]; // error!
// ----------------> ~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type '{ name: "mistake"; verifier: (a: string, b: number) => string; }'
// is not compatible with type 'ValidationCheck'.
// string cannot be assigned to boolean
Since validationChecks
has not been expanded, the compiler can perform more accurate type checks:
const someCheck = validationChecks[Math.floor(Math.random() * validationChecks.length)];
/* const someCheck: {
readonly name: "required";
readonly verifier: (x?: string) => boolean;
} | {
readonly name: "greaterThan";
readonly verifier: (entry: number, maximum: number) => boolean;
} */
if (someCheck.name === "greaterThan") {
someCheck.verifier(1, 2); // allowed
}
The compiler recognizes that someCheck
is a discriminated union where the name
property helps determine the type of verifier
.
Playground link to code