# TypeScript Conditional Scenarios

I started on my TypeScript journey in December 2019. I'm still finding interesting bits that I want to explore and understand more. One such find was a  [tweet by Erik Rasmussen](https://twitter.com/erikras/status/1375385794286866436) noting how the Material-UI team leverages types to enforce passing an array of values to a Select input when the `multiple` prop is true. I found this fascinating and it lead me down a bit of a rabbit hole. In this post, I dig a bit further into conditional and dependent TypeScript types to further my own understanding. 

### Scenario 1: Optional Modifier
If you have worked with TypeScript at all, you are likely aware of the * [optional modifier](https://www.typescriptlang.org/docs/handbook/2/objects.html#optional-properties) *. It designates a property as being required or not while still preserving type. Here is an example straight from the TypeScript docs: 
```
interface PaintOptions {
  shape: string;
  xPos?: number;
  yPos?: number;
}

let p0: PaintOptions = {shape: 'square'} // valid
let p1: PaintOptions = {shape: 'square', xPos: 1} // valid
let p2: PaintOptions = {shape: 'square', xPos: 1, yPos: 1} // valid
let p3: PaintOptions = {shape: 'square', yPos: 'asdf'} // invalid
```
In the above example, `xPos` and `yPos` are optional, BUT if present, must be numbers. This is a fairly rudimentary condition.

### Scenario 2: Union Types
Another basic TypeScript concept is * [Union Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types) *. This allows us to define a set of allowed types or literal values a property can be. Let's modify the above example to only allow the `Shape` property to be either literal `'circle'` OR `'rectangle'` and allow `yPos` to be either a number OR a string.

```
interface PaintOptions {
  shape: 'circle' | 'rectangle'; // required literals
  xPos?: number; // optional, but must be number
  yPos?: number | string; // optional but must be a number or string
}

let p3: PaintOptions = {shape: 'rectangle', yPos: 'asdf'} // valid now
let p4: PaintOptions = {shape: 'circle', yPos: 'asdf'} 
let p5: PaintOptions = {shape: 'triangle', yPos: 'asdf'} // invalid because we don't allow shape to be 'triangle`
```

### Scenario 3: Using Union Types for Distinct Property Sets
In Scenario 2 above, we leveraged Union Types for simple conditionals using scaler types and literals. We can also union more complex types. In this example, we'll introduce two object types `Circle` and `Rectangle` to encapsulate the properties that define them both. We'll then attempt to make a Union Type that allows either of these types. 

```
type Circle = {radius: number};
type Rectangle = {height: number, width: number};

type Shape = Circle | Rectangle;
```
With the above, you might assume that a shape can be *either* `Circle` *or* `Rectangle`. However, we actually end up with a mishmash that allows *any* of the types from both.

```
let circle: Shape = {radius: 5}
let rectangle: Shape = {height: 2, width: 4} 
let weird: Shape = {height: 2, radius: 5}; // unexpectedly valid
let weird2: Shape = {height: 2, width: 6, radius: 5}; // unexpectedly valid
```
In this case, the `Shape` type is a Union of the *individual properties* rather than the the Types themselves. This might be useful for some applications, but doesn't help us create the type of conditional logic we'd expect. 

*** Enforcing Conditionals with literals ***

With the above, we can utilize a literal type on our individual types to create the conditions we expect. We'll give both `Circle` and `Rectangle` a new property `name` and give them the literal values 'circle' and 'rectangle' respectively. 

```
type Circle = {name: 'circle', radius: number};
type Rectangle = {name: 'rectangle',  height: number, width: number};
type Shape = Circle | Rectangle;
```
Now when we use the same examples above, we get:
```
let circle: Shape = {name: 'circle', radius: 5} 
let rectangle: Shape = {name: 'rectangle', height: 2, width: 4} 
let weird: Shape = {name: 'circle', height: 2, radius: 5}; // now invalid
let weird2: Shape = {name: 'rectangle', height: 2, width: 6, radius: 5}; // now invalid
let weird3: Shape = {name: 'circle', height: 2}; // also invalid
```
Our weird Frankenstein type now behaves as expected: Circles can't have height. Rectangles don't have a radius. 

🤔 By adding the `name` property with literal values, I believe TypeScript is able to use a form of const assertions to prevent type widening, however, I'm not 100% on this terminology in this case. If you know more accurately what is going on here, feel free to post in the comments. 


### Scenario 4: Exclusive OR within a Type
In the above example, we created a type `Shape` that allowed a variable to be one of two distinct types `Circle` or `Rectangle` but not both. A similar example to this might be to require two properties within a *single* type to be exclusive to each other. 

For this scenario, we'll introduce a new type `BorderSpec` with two logically conflicting properties `borderWidth` and `borderless`. This example might be a bit contrived, but pretend we can't have a `borderWidth` if `borderless` is `true` and vice versa.

```
type BorderSpec = {borderWidth?: never, borderless: true}
    | {borderWidth: number, borderless?: never | false}

let b1: BorderSpec = {borderWidth: 1};
let b2: BorderSpec = {borderless: true};
let b3: BorderSpec = {borderless: false, borderWidth: 1};
let b4: BorderSpec = {borderless: true, borderWidth: 1}; // Invalid - can't be borderless and have a width
```

This mostly builds off of the previous examples leveraging union types, optional modifiers, and literals. However, the new concept here is the ` [never](https://www.typescriptlang.org/docs/handbook/basic-types.html#never) ` type. This type is mostly used as a return type for functions that always throw exceptions. However, we use it here to say that the subtype can *never* have the specific property present. It can't be null even.

Tying all the examples together thus far we get:

```
type Shape = BorderSpec & (Circle | Rectangle);

let circle: Shape = {name: 'circle', radius: 5, borderless: true} 
let rectangle: Shape = {name: 'rectangle', height: 2, width: 4, borderless: true} 
let ring: Shape = {name: 'circle', radius: 5, borderWidth: 1} 
let frame: Shape = {name: 'rectangle', height: 2, width: 4, borderless: false, borderWidth: 1} 
```
I think this is pretty neat! 

*** Let's Make it Generic***
Let's take everything we just did and utilize another tool in the TypeScript toolbox:  [generics](https://www.typescriptlang.org/docs/handbook/2/generics.html). I won't go into how they work, but it should be evident by the example of what they do.

```
type Shape<T> = BorderSpec & T; // Was a Union Type

let circle: Shape<Circle> = {name: 'circle', radius: 5, borderless: true} 
let rectangle: Shape<Rectangle> = {name: 'rectangle', height: 2, width: 4, borderless: true} 
let ring: Shape<Circle> = {name: 'circle', radius: 5, borderWidth: 1} 
let frame: Shape<Rectangle> = {name: 'rectangle', height: 2, width: 4, borderless: false, borderWidth: 1} 
```
This doesn't give us anything we didn't already have, but does decouple the `Shape` type from `Circle` and `Rectangle` so we can introduce more shapes later in a less intrusive way. It also sets up the next example a bit.

### Scenario 6: Ternary Operations on Boolean Generics ###
Let's take a step back and simplify our code. A  [ternary operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator)  is one that takes 3 inputs - a conditional statement, a statement if the condition is met, and a statement if the condition is not met. I generally hate seeing these in the wild. However, one benefit of them is that they themselves are a single statement - unlike a proper if/then condition. As such, they can be used inline when an if/then might not otherwise be allowed ... such as in a type definition.

To demonstrate this, we will make a generic type `Boolish` that "returns" a string stating if the argument to the generic is boolean true.

```
type Boolish<T> = T extends boolean ? 'isBool' : 'notBool';

// Try it out
let f0: Boolish<false> = 'isBool';
let f1: Boolish<0> = 'notBool';
let f2: Boolish<null> = 'notBool';
let f3: Boolish<undefined> = 'notBool';

let t0: Boolish<true> = 'isBool';
let t1: Boolish<1> = 'notBool';
```

Given the above, we have a new conditional tool available to us. 

🤔 As you might be able to tell from the code, I wanted to test if other types evaluated to a "truthy" value, but after some exploring, I was unable to get it to work. I may investigate further. 


### Scenario 7: Select Input
With the above scenarios out of the way, we arrive at the reason reason I started down this path - what is going on in the Material-UI Select input that allows `value` to be an array when `multiple` is true? 

First, we'll de-reactify the code from the original Tweet so it is pure TypeScript. 

```
type Value<T, Multiple> = Multiple extends true ? Array<T> : T;

interface InputProps<T, Multiple extends boolean | undefined> {
    value: Value<T, Multiple>;
    multiple?: Multiple;
}

function Input<T, Multiple extends boolean | undefined>(props: InputProps<T, Multiple>) {
    return 'whatever'
}
```
Picking this apart a bit:

- The `Value` Generic type takes in two type arguments: `T` and `Multiple`. `T` is used to type the Value (i.e. string, number, etc). If `Multiple` is true (and thus our conditional expression evaluates to true), the "output" type is an Array of elements of type `T`. Otherwise, it is just `T` (i.e. a single scalar type).

- The `InputProps` interface is also generic and takes in the same `T` as above and technically the same `Multiple` as above, but allows it to be undefined. This then sets its property `value` to be our `Value<T, Multiple>` (again an Array of type `T` or simply `T`).

- Finally, the `Input` function takes in a props argument that must match the above Generic conditions.


Let's try it out:
```
// Props
let multiNumProps: InputProps<number, true> = {value: [1,2,3,4], multiple: true}; // valid
let stringProps: InputProps<string, false> = {value: 'asdf', multiple: false}; // valid
let stringPropsImplicit: InputProps<string, undefined> = {value: 'asdf'}; // valid
let invalidMultiProps: InputProps<number, false> = {value: [1,2,3,4], multiple: false}; // invalid: Not multiple but array given

// Input
let c1 = Input<number, true>(multiNumProps); // Valid
let c2 = Input<string, false>(stringProps); // Valid
let c3 = Input<string, undefined>(stringPropsImplicit); // Valid
let cInvalid = Input<string, false>(multiNumProps); // Invalid because expecting string, not multi
```

That's pretty neat.

This all seems pretty verbose, but this was designed for React, which obfuscates the typing a bit so we get the enforcement, but also get the clean interface of React.


### Wrapping Up
TypeScript is a a great way to enforce consistency in your code. By leveraging some of these conditional situations, you can further reduce the number of bugs. As always, TypeScript runs at compile time, so always be sure to use run time validation for user input. 

If you want to explore some additional scenarios, check out these links:
- [Is it possible to restrict number to a certain range](https://stackoverflow.com/questions/39494689/is-it-possible-to-restrict-number-to-a-certain-range) *stackoverflow
-  [React.js TypeScript Conditional Props - Props that depend on other Props](https://www.youtube.com/watch?v=vXh4PFwZFGI)  *youtube*


Happy Coding.








