3

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

0

3 Answers 3

3

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.

Sign up to request clarification or add additional context in comments.

3 Comments

@piotr.schaar OP this is a more efficient answer.
To be fair, @catchergeese's answer is just as valid (even provides better type checking inside the function but requires a different signature.
yea I don't think his answer is wrong. Just that function overloading is more readable at first glance plus you'd still have to destructure options within his function.
3

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.

Playground link to code

Comments

0

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
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.