2 ways to create a Union from an Array in Typescript
Tags: typescript
Published: 2019-12-27 21:09:37

Let's say we have a simple interface used to describe an object.


```ts
interface Person {
    name: string;
    age: number;
    address: string;
};

// This is a valid 'Person' since the object contains all of the
// keys from the interface above and the types allline up.
const person: Person = {name: "shane", age: 21, address: "England"}
```

Sometimes though, we may need to derive a NEW interface based on the old one, but restricted to certain keys. 

We often do this in API responses when limiting the data that's returned.

As a simple example, let's look at how to create a new interface with only the `name` & `age` fields from the `Person` interface. 

## Deriving a new type

Of course in such a simple example with only 3 fields, we can and probably should just make a new interface like this:

```ts
interface Person2 {
    name: string;
    age: number;
};
```

But as we all know, interfaces get much, much larger in real codebases - so we really want to re-use the `name` and `age` fields & their types.

One way to do this would be to `pick` the fields you want to keep

```ts
type Person2 = Pick<Person, "name" | "age">
```

This gives us exactly what we wanted from above, since `Pick` uses the elements of the `"name" | "age"` union to create a new mapped type. It's the same as if we wrote the following manually:

```ts
type Person2 = {[K in "name" | "age"]: Person[K]}
```

The really important part here being that we're deriving a new type, rather than hard-coding a new one. That means subsequent changes to the underlying `Person` interface will be reflected throughout our codebase. 

For example, if the `user` property changes in `Person` to be an `object` rather than a `string` - that change would propagate through to our new `Person2` type.

## Mapped types are pretty amazing

As you can probably tell from above, deriving new types from existing ones is an extremely powerful concept - one you'll end up using more and more as you increase your Typescript knowledge.

Now, looking back at the code samples above, what would happen if you wanted to replace the union `"name" | "age"` with a dynamic array instead?

You can imagine a function that takes an array of keys as strings, and returns an object with only those keys present.

```ts
// Here's the goal: we want Typescript
// to know that `p` here is not only a subset of `Person`
// but also it ONLY contains the keys "name" & "age"
const p = getPerson(["name", "age"]);
```

The first approach might be to accept `string[]` for the fields, and then return a `Partial<Person>`

```ts
function getPerson(fields: string[]): Partial<Person> {
   // implementation omitted for brevity
   return {} as any;
}
```

There are 2 main problems with this:

1. The `fields` argument can contain ANY strings here, but we want to narrow that to only allow keys from the `Person` interface.
2. The return type of `Partial<Person>` loses type information as it makes all fields optional.


## Hard-code it first

To understand how to solve both of those problems, let's rewrite the function signature to include exactly the types we wanted in our example.

```tsx
function getPerson(fields: ("name" | "age")[]): Pick<Person, "name" | "age"> {
   // implementation omitted for brevity
   return {} as any;
}
```

Although it's becoming a bit of a token-soup now, looking at the `fields` argument first you can see that it's not `string[]` that we want (since that type include EVERY string) it's actually just "an array of keys that exist in person".

This is all about understanding how Typescript can create & consume union types & how to combine them with Array types.

```ts
// This is a "union" of 3 string literals
type KeyUnion = ("name" | "age" | "person");

// This is an Array type, where it's elements are 
// restricted to those that are found in the union 
// "name" | "age" | "person"
type KeyArray = ("name" | "age" | "person")[];

// Sometimes it's easier to understand in this style
type KeyArray = Array<"name" | "age" | "person">;

// Finally you can substitute the strings for the
// type alias created above
type Keys = KeyUnion[]
```

That's a good primer on some basic union/array types - those bits are required to understand why the next bit will work.

Now though, we want to avoid the hard-coded string literals altogether. 

It would be extremely tedious to have to maintain a list of strings that refer to keys on an interface and luckily Typescript has plenty of convenience types to help us here. 

```ts
// Before: hard-coded keys
type KeyUnion = ("name" | "age" | "person");

// After: a 'derived' union type based on 
// the Person interface
type KeyUnion = keyof Person;
```

Whilst those 2 are equivalent, it's clear which is the best to use. So, applying this piece back to our function, we get the following:


```ts
function getPerson(fields: (keyof Person)[]): Pick<Person, keyof Person> {
   // implementation omitted for brevity
   return {} as any;
}
```

## Not there yet.

This DOES provide type safety for the function *call* site - you won't be able to call `getPerson` with a key that doesn't exist in the interface any more. 

```ts
// Type 'string' is not assignable to type '"name" | "age" | "address"'
getPerson(["name", "age", "location"])
```

Above, TS correctly prevents this, since the 3rd element in the array is not a valid key on `Person`.

But, the *return* type is now off. 

If we return to `mapped types` for a moment, we'll easily see why `<Pick, keyof Person>` is not what we want here.

```ts
// this creates a new type with ALL the keys in Person
Pick<Person, keyof Person>

// it's exactly the same as this, it's a 1:1 mapping
{[K in keyof Person]: Person[K]}

// Or, for even more clarity (not valid typescript though), it's like doing this
{[for K in "name" | "age" | "person"]: Person[K]}
```

So now it's clear, we do still want `Pick`, but we need to create a union type based on the array of keys passed in - essentially we need the following...

```ts
const keys = ["name"]
// -> should produce Pick<Person, "name">

const keys = ["name", "age"]
// -> should produce Pick<Person, "name" | "age">

const keys = ["name", "age", "address"]
// -> should produce Pick<Person, "name" | "age" | "address">
```


## First way to create a union from an array

As a standalone example, providing we give a type assertion like this, we can easily get a union of string literals from the following:

```ts
const keys: (keyof Person)[] = ["name", "age"]
```

By restricting the type with `(keyof Person)[]` (rather than just `string[]`) it means that we can ask Typescript nicely to derive a union from this.

```ts
// creates the union ("name" | "age")
type Keys = typeof keys[number]
```

It turns out that using `typeof keys[number]` on *any* array will force Typescript to produce a union of all possible types within.

In our case, since we indicated that our array can only contain keys from an interface, Typescript uses this information to give us the union we need.

For clarity, the following would NOT work

```ts
const keys = ["name", "age"]

// only creates the union `string` since there's no 
// type assertion after `keys` 
type Keys = typeof keys[number]
```

## Second way to create a union from an array

Union types in Typescript really are the key to unlocking so many advanced use-cases that it's always worth exploring ways to work with them.

To complete our function from above, we'll actually be going with the first example, but it's worth knowing this other trick in case you come across a similar situation.


### *const* assertions

Using a relatively new feature, you can instruct TS to view arrays (and other types) as literal (and readonly) types.

You see, Typescript infers that the `keys` variable below has the type `string[]` which is technically correct, but for our use-case, it means that valuable information is lost if we create a union from it.
 
```ts
// to TS, this is just `string[]`...
const keys = ["name", "age"];

// ... which means this union 
// type ends up with just `string`, 
// which includes every single string ever :(
type Keys = typeof keys[number];
```

But, with a `const assertion`, we get a very different result. You're basically telling Typescript to not allow widening of any literal types. 

So `"name"` stays as `"name"`, and not `string` - which means, if we circle back to the previous example, we can now ask Typescript to create a union of all elements.

```ts
// now, TS thinks this is `readonly ["name", "age"]`
const keys = ["name", "age"] as const;

// this is now the union we wanted, "name" | "age"
type Keys = typeof keys[number];
```

## Finishing the implementation

Finally, let's put the knowledge to work.

To recap, we wanted to use Typescript to annotate a function such that when we call the function with an array of strings, it validates that the strings all match key names on an interface, and then use those dynamic values to create a narrowed return type derived from the original interface.


```ts
// only accept keys from `Person`
// then use them to narrow a new type. 
function getPerson<T extends (keyof Person)[]> (fields: T): Pick<Person, T[number]> {
   // implementation omitted for brevity
   return {} as any;
}
```