Filter tuple values from a Typescript tuple.
Published: 2023-08-03 14:51:07

The other day, I was writing a filter function in an Angular application that takes in a ReadOnly array in Typescript and filters it by a single value (essentially a remove function). 
I was annoyed that I needed to cast the resulting object without the value, and wondered if it was possible to write a type that excludes values from a tuple. 
Obviously the `Array.prototype.filter` isn't a good candidate to test this out on, since it takes in a predicate and you can't infer individual values from the default `filter` implementations. 

I landed on either a custom remove function, or using declaration merging to add a definition to the `lodash/without` function.
Eventually, the requirements to this approach started forming:
I needed a type mapping that takes in both regular tuples and readonly tuples, and an array of values that needed to be removed. I started simpler, however, with just a single value type to be removed.

In case you're unaware, if a type mapping takes in a readonly array-like, it also accepts regular tuples:

```typescript
type TypeMap<Arr extends readonly unknown[]> = ...
const one = ['1', '2'] as const;
type One = TypeMap<typeof one>; // readonly ['1, '2'];
type Two = TypeMap<['1', '2']>; // ['1', '2'];
```
Both are accepted.

My naive approach was similar to previous attempts to do array manipulations in Typescript types, which is to use recursion. First, write the inputs to the 'function':

```typescript
type Without<Arr extends readonly unknown[], Value extends Arr[number]> = ...
```
Value is typed as a value that is in Arr, but this doesn't actually limit the set of values that are allowed, as it would be possible to remove non-existent values from an array; nothing would happen in that case.

Next is the recursion:

```typescript
type Without<Arr extends readonly unknown[], Value extends Arr[number]> Arr extends [
  infer Head,
  ...infer Tail
]
  ? Head extends Value
    ? Tail // current element matches, so exclude it
    : [Head, ...Without<[...Tail], Value>] // otherwise keep it and continue with recursion
  : Arr // The head/tail can't be inferred, so this might be an empty array, in which case do nothing.

type Foo = Without<['1', '2'], '2'>;
```

That works! However, when using readonly tuples, we lose the modifier, because for every step in the recursion, a new tuple is created without the modifier. But, we if the array is not readonly, we shouldn't just blatantly add the modifier: we should add a check and act accordingly. 
One trick for this, is that in Typescript, a readonly array does not extend `unknown[]`, only `readonly unknown[]`.

```typescript
type IsReadonly<T extends readonly unknown[]> = T extends unknown[] ? false : true;
```

So, how do we incorporate this in our expanded type map?

```typescript
type Without<Arr extends readonly unknown[], Value extends Arr[number]> = Arr extends readonly [
    infer Head,
    ...infer Tail
]
    ? Arr extends unknown[] // if true, the array is not readonly
        ? Head extends Value
            ? Tail
            : [Head, ...Without<Tail, Value>]
        : Head extends Value // the array is readonly, so do the same but with readonly
            ? readonly [...Tail]
            : readonly [Head, ...Without<Tail, Value>]
    : Arr;

type Foo = Without<['1', '2'], '2'>; // ['1']
type ReadonlyFoo = Without<readonly ['1', '2'], '2'>; // readonly ['1']
```

That takes care of that, and this works for most purposes. However, looking at the signature of the `remove` function, it should be possible to remove multiple values at once, as (you guessed it), a tuple. So the final version looks something like this:

```typescript
export type Without<Arr extends readonly unknown[], const Values extends Arr[number][]> = Arr extends readonly [
    infer Head,
    ...infer Tail
]
    ? Arr extends unknown[]
        ? Head extends Values[number]
            ? Without<Tail, Values>
            : [Head, ...Without<Tail, Values>]
        : Head extends Values
        ? readonly [...Without<[...Tail], Values>]
        : readonly [Head, ...Without<Tail, Values>]
    : Arr;
```

The check for whether Head is in the Values tuple is simple, because Head extends the union of tuple values in the input. However, what is not so obvious is that returning just `Tail` on lines 8 and 11, skips other values in `Values`. (I am not entirely sure why this happens, if someone would like explain this in the comments, I'd be grateful). The solution I found to this, was to also filter the Values array by the currently compared element, using the same type map.

The results work:

```typescript
type foo = Without<['1', '2', '3'], ['2', '3']>; // ['1']
```

Typescript doesn't really allow for clean readable types, sadly, but I tried to explain it as clearly as possible.

The final step for me was to use this in a function. I decided on declaration merging with `lodash/without`, but you can use it as the return type to a function too.

```typescript
declare module 'lodash' {
    interface LoDashStatic {
        without<Arr extends readonly T[], const Values extends readonly T[]>(
            array: Arr,
            ...values: Values
        ): Without<Arr, Values>;
    }
}
```
Take note of `const Values extends ...`. This is a new feature in Typescript 5.0 that allows preferring inference of const values, such that values is inferred as a const tuple in the function, instead of a string array.

Hopefully someone can use this extended typing of these types of functions. I haven't extensively tested this, so if anyone notices a bug or situation where the output isn't the intended result, feel free to comment below.