peon123
peon123

Reputation: 398

conditional types parameters in typescript

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

Answers (3)

jcalz
jcalz

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.

Playground link to code

Upvotes: 3

emeraldsanto
emeraldsanto

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

catchergeese
catchergeese

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

Related Questions