# TypeScript Thursday - S01E03 - Conditional types

In [TypeScript Thursday - S01E01](https://nighttrax.dev/typescript-thursday-s01e01-generics) we learned about generic types and saw how we can make the output of a function preserve the type(s) of its input(s). For instance, we can write a `<Dropdown>` React component that automatically infers the type of its `onChange` callback based on the list of `options`:

```typescript
type DropdownProps<T> = {
  value: T;
  options: T[];
  onChange: (value: T) => void;
}

<Dropdown options={[1, 2, 3]} value={2}
  onChange={(value) => {}} />
//             ^? number
```

Now let's try to add an optional `multi` prop that will let the user select more than 1 value from the list of `options`:

```typescript
type DropdownProps<T> = {
  value: T | T[];
  options: T[];
  onChange: (value: T | T[]) => void;
  multi?: boolean;
}

<Dropdown 
  options={[1, 2, 3]}
  value={[2, 3]}
  multi
  onChange={(list) => {
    // Property 'includes' does not exist on type 'number'.(2339)
    //          VVV
    if (list.includes(42))
      console.log('You selected my favorite number!');
  }}
/>
```

We had to change the type of `onChange` to receive both a single value and a list, to account for both states of the `multi` prop. However, this causes an issue where we can't assume that we're going to get a list inside the callback when we pass the `multi` prop. The type says we could receive both, and it's up to us to do a check to narrow the type down.

Moreover, the `value` prop type is now also a union, but since there's no relationship between it and `multi` could pass the following incorrect values:

```typescript
<Dropdown
  options={[1, 2, 3]}
  multi
  value={2} // should be an array, but there's no type error
  onChange={() => {}}
/>
```

Today's episode will cover conditional types which let us define generic types that have branching results depending on their parameter types. This will allow us to change the types of the `onChange` callback and `value` prop above depending on whether the `multi` prop is passed in or not.

<div data-node-type="callout">
<div data-node-type="callout-emoji">✏</div>
<div data-node-type="callout-text">Upcoming episodes will cover a couple of other ways to achieve the same effect. Stay tuned!</div>
</div>

We'll also create the opposite of the `Pick` mapped type (covered in [TypeScript Thursday - S01E02](https://nighttrax.dev/typescript-thursday-s01e02-mapped-types)), create recursive types, extract types from 3rd party code, and more!

## Conditional types

Let's look back at the example above, and write the code to implement it without worrying about the types just yet:

```typescript
function Dropdown(props: any) {
  const onChange = (value: any) => {
    if (props.multi) {
      props.onChange([...props.value, value]);
    } else {
      props.onChange(value);
    }
  }

  return <select onChange={e => onChange(e.target.value)}>
    ...
  </select>;
}
```

The code is pretty straightforward: we start with an initial value for the selection, and we either replace it or append to it when the user selects a new option. We could simplify the `if-else` using a ternary expression:

```typescript
props.onChange(props.multi
  ? [...props.value, value]
  : value
);
```

Conditional types look very similar: they have a condition, a truthy branch, and a falsy branch. Let's start with a simple example before tackling the `onChange` type:

```typescript
type Foo<T> = T extends string ? number : boolean;

Foo<string>; // number
Foo<'a'>; // number
Foo<42>; // boolean
```

You can have more than one branch in a conditional type, so we can easily build a generic type that returns the name of a type as a string literal type:

```typescript
type TypeName<T> = T extends string ? 'string' :
  T extends number ? 'number' :
  T extends boolean ? 'boolean' :
  T extends undefined ? 'undefined' :
  T extends null ? 'null' :
  T extends Function ? 'function' :
  T extends unknown[] ? 'array' :
  'object';

TypeName<'a'>; // 'string'
TypeName<[]>; // 'array'
TypeName<null>; // 'null'
```

When we don't have a useful type to return from the falsy branch, the convention is to return the built-in `never` type which can't be assigned to any other type (stay tuned for a future episode that will cover this type, and others, in more detail):

```typescript
type KeepStrings<T> = T extends string
  ? T : never;

KeepStrings<string>; // string
KeepStrings<number>; // never
```

Now, let's try to improve our `onChange` and `value` types:

```typescript
type DropdownProps<Option, Multi extends boolean = false> = {
  options: Option[];
  multi?: Multi;
  value: Multi extends true
    ? Option[]
    : Option;
  onChange: Multi extends true
    ? (values: Option[]) => void
    : (value: Option) => void;
}
```

We declare a type with 2 generic type parameters and use a conditional type to check whether the `multi` prop is passed in as `true`. If it is we'll set the `onChange` type to receive a list of values, otherwise it will receive a single one. Similarly, the `value` prop will choose between a list and a single value.

```typescript
<Dropdown
  multi
  options={[1, 2, 3]}
  value={[2, 3]}
  onChange={(value) => {}} />
  //           ^? number[]
```

The default `= false` is needed so we can choose the single select branch in the conditional type when the `multi` prop is omitted.

```typescript
<Dropdown
  options={[1, 2, 3]}
  value={2}
  onChange={(value) => {}} />
  //           ^? number
```

Besides the specialized `onChange` and `value` types, we now also get a type error if there's a mismatch between them:

```typescript
<Dropdown
  options={[1, 2, 3]}
  // Type 'number[]' is not assignable to type 'number'.(2322)
  value={[2, 3]}
  onChange={(value) => {}} />
  //           ^? number
```

## Assignability

Generic conditional types can take either branch depending on their input and you can't know which will be selected until you instantiate the generic type parameters. Once a branch is selected you can safely assign to the resulting type:

```typescript
type Foo<T> = T extends string ? number : boolean;
//                                 ^         ^
//                                 |         |
const bar: Foo<string> = 42; // ---+         |
const baz: Foo<number> = true; // -----------+
```

When a branch hasn't been selected yet the generic conditional type is in a "deferred" state and you can assign to it only under very specific circumstances:

1. The type on the right side of `extends` is not used in either the truthy branch or the falsy branch.
    
2. The type on the right side of `extends` is not making use of `infer`.
    
3. The type you're trying to assign to the conditional type can be assigned to both the truthy branch and the falsy branch.
    

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">These restrictions were added in <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/microsoft/TypeScript/pull/46429" style="pointer-events: none">microsoft/TypeScript/#46429</a> and released as part of TypeScript version 4.5.</div>
</div>

```typescript
type B = string;
type C = number | boolean;
type D = boolean;

// B doesn't use `infer`.
type Foo<A> = A extends B ? C : D;

function foo<A>(data: A): Foo<A> {
  // `boolean` is assignable to both C and D.
  return false;
}
```

If the types `C` and `D` don't have any overlap then we can't assign anything to the conditional type, even if we use runtime checks to try and narrow down the branch that should be selected:

```typescript
type Foo<T> = T extends string ? number : boolean;
//                         ^
//                         |
//                         +--------------+
function foo<T>(data: T): Foo<T> { //     |
  if (typeof data === 'string') { // -----+
    // This should narrow down the conditional type to
    // the truthy branch, but it doesn't:
    // Type '42' is not assignable to type 'Foo<T>'.(2322)
    return 42;
  } else {
    // Type 'true' is not assignable to type 'Foo<T>'.(2322)
    return true;
  }
}
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/microsoft/TypeScript/issues/33014" style="pointer-events: none">microsoft/TypeScript#33014</a> is exploring improvements in this area and is currently part of <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/microsoft/TypeScript/issues/54298" style="pointer-events: none">the 5.2 version roadmap</a>.</div>
</div>

In the meantime, we can:

* Suppress the errors with `@ts-ignore` or `@ts-expect-error`.
    
* Use type assertions to "cast" to the truthy or falsy branch types.
    
* Use function overloads, see [TypeScript Thursday - S01E04](https://nighttrax.dev/typescript-thursday-s01e04-function-overloads) for more details!
    

If we take the `<Dropdown>` component implementation from the previous section and use type assertions to work around the errors we end up with the following:

```typescript
function Dropdown<Option, Multi extends boolean>(
  props: DropdownProps<Option, Multi>
) {
  const onChange = (value: Option) => {
    if (props.multi) {
      const multiProps = props as DropdownProps<Option, true>;

      multiProps.onChange([...multiProps.value, value]);
    } else {
      const singleProps = props as DropdownProps<Option, false>;

      singleProps.onChange(value);
    }
  }

  return <select onChange={e => onChange(e.target.value)}>
    ...
  </select>;
}
```

When it comes to assigning "deferred" conditional types to other types, we can do that as long as the destination type is a union covering both the conditional's truthy and falsy branches:

```typescript
function foo<T>(x: T): T extends boolean ? string : number { /* ... */ }
 
function bar<T>(data: T) {
  const a = foo(data);

  // This is OK.
  const b: string | number = a;
}
```

## Constraints

In [TypeScript Thursday - S01E01](https://nighttrax.dev/typescript-thursday-s01e01-generics) we saw how we can constrain a generic type parameter to ensure it meets some requirements:

```typescript
function sortById<T extends { id: number }>(values: T[]) {
  return values.slice().sort((a, b) => a.id - b.id);
}
```

The constraint carried over inside the body of the function to allow us to access the `id` property safely. For conditional types, the constraint carries over into the truthy branch:

```typescript
type ExtractId<T> = T extends { id : unknown } ? T["id"] : never;

type Foo = { id: number };
type Bar = ExtractId<Foo>;
//    ^? number
```

In the example below we check if the input type is an array and then use an indexed access type (covered in [TypeScript Thursday - S01E02](https://nighttrax.dev/typescript-thursday-s01e02-mapped-types)) to get the value type. If the input type is not an array, we leave it unchanged.

```typescript
//                                +--------+
//                                |        |
//                                V        V
type Reduce<T> = T extends unknown[] ? T[number] : T;
 
type Foo = Reduce<string[]>;
//    ^? string
 
type Bar = Reduce<number>;
//    ^? number
```

Continue reading to see how we can avoid the indexed access type, and instead infer the type of the array directly.

## Inferring in conditional types

Let's take our earlier example from the constraints section and update it to infer the array type directly using the `infer` keyword:

```typescript
type Reduce<T> = T extends Array<infer U> ? U : never;
 
Reduce<string[]>; // string
Reduce<number>; // never
```

`infer` works similarly to how TypeScript can infer generic type parameters from function calls (see [TypeScript Thursday - S01E01](https://nighttrax.dev/typescript-thursday-s01e01-generics) for more details), but it doesn't require defining those parameters upfront.

In the example below we infer both the argument type of a function, but also its return type:

```typescript
//                                       V           V
type FuncTest<T> = T extends (...args: infer A) => infer R
  ? { input: A, expected: R }
  : never;

function createFuncTest<T extends (...args: any[]) => any>(
  func: T,
  tests: FuncTest<T>[]
) {
  tests.forEach(test => {
    expect(func(...test.input)).toEqual(test.expected);
  });
}

const sum = (a: number, b: number): number => a + b;

createFuncTest(sum, [
  { input: [2, 2], expected: 4 },
  { input: [-1, 1], expected: 0 },
]);
```

TypeScript comes with built-in types `Parameters<T>` and `ReturnType<T>`, so we can simplify `FuncTest` to the following:

```typescript
type FuncTest<T extends (...args: any[]) => any> = {
  input: Parameters<T>;
  expected: ReturnType<T>;
}
```

We can use the conditional type in the function definition as we did above, but we can also use it when creating input data for the function:

```typescript
const tests: FuncTest<typeof sum> = [
  { input: [2, 2], expected: 4 },
  { input: [-1, 1], expected: 0 },
];
```

The `infer` keyword can be followed by another optional `extends` to create a nested conditional without actually having to write all the branches:

```typescript
type ReturnIfString<T> = T extends (
  (...args: any) => infer R extends string
) ? R : never;

ReturnIfString<() => 'a' | 'b'>; // 'a' | 'b'
ReturnIfString<() => number>; // never
```

`infer` can come in handy when you're dealing with 3rd party code that doesn't export a type that you need. Let's assume you're using a React component and would like to get the type of its props.

```typescript
// This is not exported, but we need it.
type Props = { foo: number };

export const Component = (props: Props) => null;

type ExtractProps<T> = T extends (props: infer Props) => any
  ? Props : never;

type MyExtractedProps = ExtractProps<typeof Component>;
//         ^? { foo: number }
```

## Distributive conditional types

If we use a conditional type on a union, the `extends` condition will be distributed over each member in the union, rather than being applied to the union itself:

```typescript
type FilterPromises<T> = T extends Promise<any> ? T : never;

type Result = FilterPromises<Promise<string> | number>;
//    ^? Promise<string>
```

The `extends Promise<any>` condition was applied to `Promise<string>` and `number`, transforming the latter into `never` and keeping the former as-is. The results were merged into a new union, `never` was dropped, and the result is `Promise<string>`. Without distributivity, the result would've been `never` because the full union is not assignable to a `Promise`.

Flipping the branches allows us to remove `Promise`s instead of keeping them:

```typescript
type RemovePromises<T> = T extends Promise<any> ? never : T;

type Result = RemovePromises<Promise<string> | number>;
//     ^? number
```

We can add a second generic type parameter to use in the condition and create reusable `Exclude` and `Extract` types:

```typescript
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;

type Foo = Exclude<string | number, number>;
//    ^? string

type Bar = Extract<string | number, number>;
//    ^? number
```

Both of the above are built-in TypeScript types, and so is `Omit` which uses the mapped type `Pick` in combination with `Exclude` to remove a set of keys from an object type:

```typescript
type Omit<T, K extends keyof T> = Pick<
  T,
  Exclude<keyof T, K>
>;

type Person = { age: number; name: string };

type Result = Omit<Person, 'age'>;
//     ^? { name: string }
```

> Note: The version above is stricter than TypeScript's built-in `Omit` type which doesn't constrain the excluded keys to be part of the type (it's missing the `extends keyof T` constraint), so the compiler won't catch typos or mistakes:
> 
> ```typescript
> type ProbablyATypo = Omit<{ foo: number }, 'fooooo'>;
> ```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/microsoft/TypeScript/issues/30825#issuecomment-523668235" style="pointer-events: none">microsoft/TypeScript#30825</a> explains the reasoning behind keeping the type loose.</div>
</div>

`Omit` can be useful when partially applying a function with object inputs:

```typescript
type Filter = {
  start: Date;
  end: Date;
  label: string;
}

const getData = (filter: Filter) => { /* ... */ }

type FilterFromNow = Omit<Filter, 'start'>;

const getDataFromNow = (filter: FilterFromNow) => {
  return getData({ ...filter, start: new Date() });
}
```

By using generic type inference we can write a [Redux](https://react-redux.js.org/using-react-redux/connect-mapstate)\-style connector function that partially applies a React component:

```typescript
type ComponentType<Props> = (props: Props) => any;

function connect<Props>(
  Component: ComponentType<Props>
): <K extends keyof Props>(
  mapStateToProps: (state: any) => Pick<Props, K>
) => ComponentType<Omit<Props,K>> {
  /* ... */
}

type Props = { foo: number; bar: number };
const Foo = (props: Props) => null;

//                  removes `foo` from
//                   the Props type
//                   +------------+
//                   |            |
//                   V            V
const Bar = connect(Foo)(() => ({ foo: 42 }));
//     ^? ComponentType<{ bar: number }>
```

Distributivity helps us build the useful types above, but if you want to avoid it you can wrap both the type you're testing in the conditional and the one you're testing against in a tuple (or any kind of object for that matter, but a tuple is the simplest):

```typescript
type Distributive<T> = T extends number ? T : never;
//                         wrap both sides
//                         V             V
type NonDistributive<T> = [T] extends [number] ? T : never;

type Foo = Distributive<number | string>;
//    ^? number

type Bar = NonDistributive<number | string>;
//    ^? never
```

## Filtering keys in mapped types

The `Omit` type shown above lets us exclude certain keys from a type based on their names, but we can use conditional types to exclude keys based on the values associated with those keys.

[TypeScript Thursday - S01E02](https://nighttrax.dev/typescript-thursday-s01e02-mapped-types) covered mapped types which can take the keys from one type and copy them over to a new type. We also saw how we can remap the keys using the `as` keyword:

```typescript
type Analytics = Record<string, { title: string; views: number }>;

type ViewsByTitle<T extends Analytics> = {
  [K in keyof T as T[K]['title']]: T[K]['views']
}
```

If we remap a key to the special type `never` it will be dropped from the result:

```typescript
type DropAllKeys<T> = {
  [K in keyof T as never]: T[K]
}

type Foo = DropAllKeys<{ foo: number }>;
//    ^? {}
```

If we add conditional types into the mix we can selectively drop certain keys if their values don't pass the condition:

```typescript
type FilterMethods<T> = {
  [K in keyof T as (
    T[K] extends Function ? K : never
  )]: T[K]
}

type Foo = {
  bar: () => void;
  baz: number;
}

type Bar = FilterMethods<Foo>;
//    ^? { bar: () => void }
```

Because we're still using `keyof` to get the list of keys from the input type, the mapped type remains homomorphic, which means we can unwrap it back to the original type (see [TypeScript Thursday - S01E02](https://nighttrax.dev/typescript-thursday-s01e02-mapped-types) for more details):

```typescript
function unwrap<T>(data: FilterMethods<T>): T {}

const bar: Bar = { bar: () => {} };

let baz = unwrap(bar);
//  ^? Foo
```

## Recursive types

We can write recursive conditional types and use the condition to know when to stop recursing. For instance, we can create a recursive conditional type that uses `infer` to flatten an array of any depth:

```typescript
type Flatten<T> = T extends Array<infer U>
  ? Flatten<U> : T;

type Foo = Flatten<string[][][]>;
//    ^? string
```

We can also write a variant of `Partial` that deeply recurses into all the keys:

```typescript
type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

type Foo = { foo: { bar: number } };

type Bar = DeepPartial<Foo>;
//    ^? { foo?: { bar?: number } }
```

## Summary

Thank you for reading and I hope you enjoyed the article. Below you can find the key takeaways and a shareable sketch note. Make sure to also check out [TypeScript Thursday - S01E04](https://nighttrax.dev/typescript-thursday-s01e04-function-overloads) which covers function overloads and compares them to conditional types!

<div data-node-type="callout">
<div data-node-type="callout-emoji">🤎</div>
<div data-node-type="callout-text">If you liked this article, maybe you'd also like some of my open-source projects at <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/NiGhTTraX" style="pointer-events: none">github.com/NiGhTTraX</a>.</div>
</div>

Takeaways:

* conditional types are like `if` expressions for types
    
* they look like ternary expressions and use the `extends` keyword for the condition
    
* you can nest conditional types
    
* the truthy branch will narrow down the type according to the condition, similar to generic constraints
    
* conditional types distribute over unions, testing each member against the condition
    
* you can avoid this behavior by wrapping both sides of the `extends` keyword with square brackets `[]`
    
* you can use the `infer` keyword to do some basic type pattern matching, like extracting the type inside a `Promise`, or extracting a type from 3rd party code
    
* you can filter out keys in mapped types by remapping them to a conditional type that returns `never`
    
* TypeScript ships with a few useful conditional types: `Exclude`, `Extract`, `ReturnType`, `Parameters` and `Omit`
    

![TypeScript Thursday S01E03 Conditional Types Sketchnote](https://cdn.hashnode.com/res/hashnode/image/upload/v1685805828082/153a9210-913d-4816-b891-2baad977e91d.png align="center")
