Reputation: 398
I'm attempting to create a function where the columns
parameter depends on what type
has been selected.
function useNavigation(type: "horizontal" | "grid" | "vertical", columns?: number)
How to change that type declaration to set that columns parameter is required
when parameter type
is grid
Upvotes: 3
Views: 233
Reputation: 330466
Traditionally (pre TS 2.8 or TS 3.0) you could only use overloads to achieve this, but now I'd be inclined to use a tuple-typed rest parameter; specifically a union of such tuple types:
function useNavigation(...args:
[type: "horizontal" | "vertical", columns?: number] |
[type: "grid", columns: number]
) { }
That means the arguments to useNavigation()
must either be: a pair where the first element is "horizontal"
or "vertical"
and the second element is an optional number
value; or a pair where the first element is "grid"
and the second element is a required number
value.
You can test that it works as desired:
useNavigation("horizontal") // okay
useNavigation("vertical", 20); // okay
useNavigation("grid", 10); //okay
useNavigation("grid"); // error!
// ---------> ~~~~~~
// Source has 1 element(s) but target requires 2
Note that the type signature for useNavigation()
is also taking advantage of labeled tuple elements, to show that we intend the caller to think of the first argument as having a name of type
and the second argument as having a name of columns
. Such labels are only relevant when it comes to type hints in IntelliSense and do not affect the types or runtime behavior. In particular, there is no variable named type
or columns
here; the function implementation sees only an args
array:
function useNavigation(...args:
[type: "horizontal" | "vertical", columns?: number] |
[type: "grid", columns: number]
) {
if (args[0] === "grid") {
args[1].toFixed(); // okay
}
}
You could, if you want, destructure args
into type
and columns
variables as in
const [type, columns] = args;
but there is an advantage to leaving it as args
; namely, that the compiler sees args
as a discriminated union type. Note that in the above implementation, the compiler sees that if args[0] === "grid"
, then args[1]
is definitely a number
and not possibly undefined
. Compare to the behavior when you separate args
out into two no-longer-seen-as-correlated variables:
if (type === "grid") {
columns.toFixed(); // oops, possibly undefined
}
The compiler only sees columns
as type number | undefined
regardless of whether or not you check type === "grid"
first. This might not be a big deal to you, but discriminated unions are useful enough that I wanted to point it out.
Upvotes: 3
Reputation: 4741
You can use function overloads to achieve this. In my example below, the first two declarations are specifying different arguments based on your requirements whereas the last declaration (and consequently the implementation) receives all possible arguments, which is why type
is still 'horizontal' | 'vertical' | 'grid'
.
The caller will get proper types and auto-completion, you'll simply have to do the checks inside the function to determine your logic based on the arguments.
function useNavigation(type: 'horizontal' | 'vertical');
function useNavigation(type: 'grid', columns: number);
function useNavigation(
type: 'horizontal' | 'vertical' | 'grid',
columns?: number,
) {
// Your function body...
}
Here's a TS playground demonstrating this.
Upvotes: 3
Reputation: 732
My favorite pattern to achieve some additional parameters to be required for some parameter(s) is to wrap it in an object and type it as:
function useNavigation(options: { type: "horizontal" | "vertical"; columns?: number } | { type: "grid", columns: number })
and use it as
useNavigation({ type: "horizontal" })
useNavigation({ type: "grid", columns: 12 })
It may feel a little bit repetitive but provides good typesafely, also when types are narrowed down after property check
if (options.type === "grid") {
// here typescript knows that `columns` exists
}
Upvotes: 0