How to implement a guard function isUndefined that only accept arguments whose type includes undefined? The function should assert that the argument is undefined.
Basic implementation:
function isUndefined(value: unknown): value is undefined {
return typeof value === 'undefined';
}
The expected behavior:
isUndefined(true) // <- Compilation error. argument type does not include undefined
const arg = true as boolean | undefined
isUndefined(arg) // <- No error. Argument type includes undefined.
// Compiler infers arg as undefined
arg // Hover arg in IDE => arg: undefined
It is possible to create a
isUndefinedfunction that behaves similarly to what you described. In your "expected" example, you do nothing with the result of the callisUndefined(arg)so the last value would never be inferred to bearg: undefined. I will assume that this is just a mistake.The problem
The function you are describing is supposed to be able to take a value that is a union that has to contain
undefined. This is not a common thing in typescript. As you may know typescript has generic functions, and you can restrict the parameters of generic functions using theextendskeyword. This is a basic example:In this case, the function will accept anything that is at least number. Meaning that the call with
number | stringwill error.However this is not what we are looking for. We need some sort of a constraint that will mean: "There has to be a possibility that the argument passed in will be
undefined".The solution
In typescript this is achieved by flipping the extends constraint. This is a basic example:
Of course, we cannot just do this in the generic type constraint for a function. However, we can use a little trick and use a helper generic parameter to get the result.
This is a working example:
TSPlayground link
The function is little involved so let's go over it. We cannot flip the generic constrait directly in the parameter, so we are using a helper generic parameter
Rwhich as you can see is not constraint in any way. If we try to constraint it like this..., R extends T | never ...we would get a circular reference error.As you can see though, the
Rparameter is being assigned a default value. In the value assignment, we can use the above mentioned technique, to setRto eigherTifundefinedis present inTorneverotherwise.We can then use this value of
Rto constraintT.This does not error, because values are being set before the constraits are checked.
Why the @ts-ignore
Typescript tries to enforce that the type specified in the
value is undefineddeclaration (in our caseundefined) is even possible to be passed in to this function. However, due to the complex nature of our type definition, andRnot being constraint (which means that the caller could theoretically supply their own type forRwhich would allow them to pass in a value which can never be undefined) typescript throws an error.We cannot do something like
isUndefined(value: T | undefined)because that would break the inference ofT.So as developers, we tell typescript to ignore the error, because we know better. And this is not a problem, because even if someone provided their own types, they would only get the ability to check if some value is undefined or not.
This is a walkthrough:
Example 1
Let's say we are passing in a value of
boolean | undefinedFirstly, values are assigned.
Tis assigned a value ofboolean | undefined(inferred from the argument) andRis assigned a value ofTbecauseundefined extends T.After that constraints are being checked.
Tis supposed to extendRwhich is equal toT, thereforeTis supposed to extendTwhich is valid.Everything works!
Example 2
Let's say we are passing in a value of
booleanFirstly, values are assigned.
Tis assigned a value ofboolean(inferred from the argument) andRis assigned a value ofneverbecauseundefineddoesn't extendboolean.After that constraints are being checked.
Tis supposed to extendRwhich is equal tonever, therefore the constraint forTfails.Everything works!
Note
The error procuded by this solution can be quite cryptic for someone who doesn't understand typescript very well, therefore I would suggest putting a JS Doc comment on the function explaining the
is not assignable to nevererror.Even better solution might be replacing the
nevertype with something like"The value passed in must have a chance of being undefined". This would also work, and the resulting error message would be as follows:Modified version