Narrowing Library
Tags: programming, tutorial, beginners, typescript
Published: 2022-06-20 09:53:21

TypeScript Narrowing #8

{% embed https://www.youtube.com/embed/I7ga4tL_w5I %}
_See this and many other articles at [lucaspaganini.com](lucaspaganini.com)_

Hello, welcome to the last article of our TypeScript Narrowing series!

Today, I'll show you an open sourced library that I wrote using the same techniques discussed in our previous articles. This library is by no means rigid to anyone's workflow, on the contrary, I made it with the intention of it being valuable to anyone working on a TypeScript codebase.

I'm Lucas Paganini, and on this website, we release web development tutorials. Subscribe if you're interested in that.

## Installing and Target Audience

Before we talk about what's inside, you should know how to install it and who should install it.

I made this library available on NPM, to add it to your codebase, simply run `npm install @lucaspaganini/ts`.

Now, regarding who should install it, I see this library as "Lodash for TypeScript". It provides you with flexible and type safe utilities that make your codebase cleaner. Also, everything is isolated, so you can install it and only import the things that you actually want to use.

That said, I truly believe that this library is useful for anyone who's working on a TypeScript codebase. Frontend, backend, whatever... If you're using TypeScript, you'll benefit from having those utilities.

## Currently Available Modules

Without further ado, let's explore what's currently available in the library.

👉 I say "currently available" because it's a living thing. Over time, we will add more to it.

So far, our library has 3 modules:

1. Core
2. Assertions
3. Predicates

## Core Module

Let's start with the core module.

The core module contains those 6 utilities:

1. `Mutable`
2. `NonNullableProperties`
3. `ObjectValues`
4. `PickPropertyByType`
5. `PickByType`
6. `makeConstraint`

`Mutable` is the opposite of the native `Readonly` type. It converts the readonly properties of a type into regular mutable properties.

```ts
type Mutable<T> = { -readonly [P in keyof T]: T[P] }

Mutable<ReadonlyArray<number>>
//=> Array<number>

Mutable<{ readonly a: string }>
//=> { a: string }
```

`NonNullableProperties` is similar, it converts all the properties of a type into non-nullable properties.

```ts
type NonNullableProperties<T> =
  { [P in keyof Required<T>]: NonNullable<T[P]> }

NonNullableProperties<{ a: string | null }>
//=> { a: string }

NonNullableProperties<{ b?: number }>
//=> { b: number }

NonNullableProperties<{ c: Date | undefined }>
//=> { c: Date }
```

Then we have `ObjectValues`, which returns a union type of the types of all the properties in an object. So if your object has three properties, being them a `string`, a `number` and a `Date`. `ObjectValues` will give you the `string | number | Date` type.

👉 I can't tell you how useful that is.

```ts
type ObjectValues<O> = O[keyof O]

ObjectValues<{ a: string ; b: number; c: Date }>
//=> string | number | Date
```

`PickPropertyByType` returns the keys of the properties that match the expected type.

Similar to our last example, if we have an object with four properties, one being a `string`, another being a `number` and the last two being `Date`s. We could use `PickPropertyByType` to get only the properties that are `string`s. Or the ones that are `number`s. Or even the two that are `Date`s.

```ts
type PickPropertyByType<O, T> =
  ObjectValues<{ [P in keyof O]: O[P] extends T ? P : never }>

type Test = { a: string; b: number; c: Date; d: Date }

PickPropertyByType<Test, string>
//=> "a"

PickPropertyByType<Test, number>
//=> "b"

PickPropertyByType<Test, Date>
//=> "c" | "d"
```

Similarly, `PickByType` returns an object that only contains the properties that match the expected type.

```ts
type PickByType<O, T> = Pick<O, PickPropertyByType<O, T>>

type Test = { a: string; b: number; c: Date; d: Date }

PickByType<Test, string>
//=> { a: string }

PickByType<Test, number>
//=> { b: number }

PickByType<Test, Date>
//=> { c: Date; d: Date }
```

And last but not least, `makeConstraint` allows us to set a type constraint and still keep the literal types.

```ts
const makeConstraint =
  <T>() =>
  <V extends T>(v: V): typeof v =>
    v;
```

For example, let's say we have a type called `Icon` that contains a name and an id. Both properties should be strings.

Then we declare a `ReadonlyArray<Icon>` with two icons, one with the id `"error"` and the other with the id `"success"`.

Now, if you try to extract the `IconID` based on the type of `icons`, it will be `string`. But that's too broad. `IconID` should be `"error" | "success"`.

```ts
type Icon = { id: string; name: string };

const icons: ReadonlyArray<Icon> = [
  { id: 'error', name: 'Error, sorry' },
  { id: 'success', name: 'Success, yaaay' }
] as const;

type IconID = typeof icons[number]['id'];
//=> IconID = string
```

If we remove the casting of `icons` to `ReadonlyArray<Icon>`, we get what we want, but then we lose the type safety of `icons`.

```ts
type Icon = { id: string; name: string };

const icons = [
  { id: 'error', name: 'Error, sorry' },
  { id: 'success', name: 'Success, yaaay' }
] as const;

type IconID = typeof icons[number]['id'];
//=> IconID = "error" | "success"
```

```ts
type Icon = { id: string; name: string };

const icons = [
  { id: 'error', foo: 'Error, sorry' },
  { id: 'success', bar: 'Success, yaaay' }
] as const;

type IconID = typeof icons[number]['id'];
//=> IconID = "error" | "success"
```

That's where `makeConstraint` comes into play.

```ts
const makeConstraint =
  <T>() =>
  <V extends T>(v: V): typeof v =>
    v

type Icon = { id: string; name: string }
const iconsConstraint = makeConstraint<ReadonlyArray<Icon>>()

const icons = iconsConstraint([
  { id: 'error', foo: 'Error, sorry' }, //=> Error
  { id: 'success', bar: 'Success, yaaay' }, => Error
] as const)

type IconID = typeof icons[number]['id']
//=> IconID = "error" | "success"
```

With it, we can make sure that `icons` is a `ReadonlyArray<Icon>` but still get its literal readonly types.

```ts
const makeConstraint =
  <T>() =>
  <V extends T>(v: V): typeof v =>
    v;

type Icon = { id: string; name: string };
const iconsConstraint = makeConstraint<ReadonlyArray<Icon>>();

const icons = iconsConstraint([
  { id: 'error', name: 'Error, sorry' },
  { id: 'success', name: 'Success, yaaay' }
] as const);

type IconID = typeof icons[number]['id'];
//=> IconID = "error" | "success"
```

## Assertions Module

Cool, now let's get into the assertions module.

This module contains these 4 utilities:

1. `AssertionFunction`
2. `UnpackAssertionFunction`
3. `assertHasProperties`
4. `fromPredicateFunction`

An `AssertionFunction` is exactly what it seems. A function that makes a type assertion.

```ts
const assertIsString: AssertionFunction<string> = (v) => {
  if (typeof v !== 'string') throw Error('Not a string');
};

let aaa: number | string;
assertIsString(aaa);
aaa; // <- aaa: string
```

And `UnpackAssertionFunction` returns the type asserted by an `AssertionFunction`.

```ts
const assertIsString: AssertionFunction<string> = v => {
  if (typeof v !== 'string') throw Error('Not a string')
}

UnpackAssertionFunction<typeof assertIsString>
//=> string
```

`assertHasProperties` asserts that the given value has the given properties, and throws if it doesn't.

👉 To keep things safe, the asserted properties are typed as `unknown`, check this one-minute [video](https://www.youtube.com/watch?v=k_q-NnmwGTo&ab_channel=LucasPaganini) to understand the differences between `any` and `unknown`.

```ts
let foo: unknown = someUnknownObject;

// Usage
foo.a; // <- Compilation error

assertHasProperties(['a'], foo);
foo.a; // <- foo: { a: unknown }
```

And the last utility in the assertions module is `fromPredicateFunction`. It takes a `PredicateFunction`, which we'll talk about in a second, and returns an `AssertionFunction`.

## Predicates Module

The last module in our library is also the largest. The predicates module contains 11 utilities:

1. `PredicateFunction`
2. `UnpackPredicateFunction`
3. `UnguardedPredicateFunction`
4. `AsyncPredicateFunction`
5. `AsyncUnguardedPredicateFunction`
6. `makeIsNot`
7. `makeIsInstance`
8. `makeIsIncluded`
9. `makeHasProperties`
10. `makeAsyncPredicateFunction`
11. `fromAssertionFunction`

The first one, `PredicateFunction`, is a type guard. It takes a value and returns a type predicate.

You may be tempted to call this a "type guard", but as I've mentioned in the sixth article of this series (the one about higher order guards), the "type guard" naming is very specific to TypeScript, and these types of functions have been called "predicate functions" way before TypeScript even existed.

```ts
type PredicateFunction<T = any> = (v: unknown) => v is T;

const isString: PredicateFunction<string> = (v): v is string =>
  typeof v === 'string';

let aaa: number | string;
if (isString(aaa)) {
  aaa; // <- aaa: string
}
```

Similarly to `UnpackAssertionFunction`, we can use `UnpackPredicateFunction` to extract the type guarded by a `PredicateFunction`.

```ts
type PredicateFunction<T = any> = (v: unknown) => v is T

const isString: PredicateFunction<string> =
  (v): v is string => typeof v === 'string'

type UnpackPredicateFunction<F extends PredicateFunction> =
  F extends PredicateFunction<infer T> ? T : never

UnpackPredicateFunction<typeof isString>
//=> string
```

Sometimes we have predicate functions that don't return a type predicate, they just return a regular `boolean`. For those cases, we have the `UnguardedPredicateFunction`.

For example, `isEqual` is an `UnguardedPredicateFunction`.

```ts
type UnguardedPredicateFunction<Params extends Array<any> = Array<any>> = (
  ...args: Params
) => boolean;

const isEqual = (a: number, b: number): boolean => a === b;
```

Then we have the `AsyncPredicateFunction`, `AsyncUnguardedPredicateFunction` and `makeAsyncPredicateFunction`. I won't go deeper into them because the seventh article of our TypeScript Narrowing series was all about them, so I'm not going to waste your time repeating information haha.

```ts
type AsyncPredicateFunction<T = any> = (
  value: unknown
) => Promise<PredicateFunction<T>>;

type AsyncUnguardedPredicateFunction<Params extends Array<any> = Array<any>> = (
  ...args: Params
) => Promise<boolean>;

type MakeAsyncPredicateFunction = {
  <F extends AsyncUnguardedPredicateFunction>(fn: F): (
    ...args: Parameters<F>
  ) => Promise<UnguardedPredicateFunction<Parameters<F>>>;

  <T>(fn: AsyncUnguardedPredicateFunction): AsyncPredicateFunction<T>;
};
```

`makeIsNot` was also mentioned previously, in the sixth article. It takes a `PredicateFunction` and returns the inverted version of it.

```ts
const isNumber: PredicateFunction<number> = (v): v is number =>
  typeof v === 'number';
const isNotNumber = makeIsNot(isNumber);

let aaa: number | string | Date;
if (isNotNumber(aaa)) {
  aaa; // -> aaa: string | Date
} else {
  aaa; // -> aaa: number
}
```

`makeIsInstance` is new though. It takes a class constructor and returns a `PredicateFunction` that checks if a value is an `instanceof` the given class constructor.

```ts
const makeIsInstance =
  <C extends new (...args: any) => any>(
    classConstructor: C
  ): PredicateFunction<InstanceType<C>> =>
  (v): v is InstanceType<C> =>
    v instanceof classConstructor;

// The following expressions are equivalent:
const isDate = makeIsInstance(Date);
const isDate = (v: any): v is Date => v instanceof Date;
```

`makeIsIncluded` takes an `Iterable` and returns a `PredicateFunction` that checks if a value is included in the given iterable.

```ts
const makeIsIncluded = <T>(iterable: Iterable<T>): PredicateFunction<T> => {
  const set = new Set(iterable);
  return (v: any): v is T => set.has(v);
};

// The following expressions are equivalent:
const abc = ['a', 'b', 'c'];
const isInABC = makeIsIncluded(abc);
const isInABC = (v: any): v is 'a' | 'b' | 'c' => abc.includes(v);
```

And finally, just like in the assertions module, we have `makeHasProperties` and `fromAssertionFunction`.

`makeHasProperties` takes an array of properties and returns a `PredicateFunction` that checks if a value has those properties

```ts
let foo: unknown = someUnknownObject;

// Usage
foo.a; // <- Compilation error

const hasPropA = makeHasProperties(['a']);
if (hasPropA(foo)) {
  foo.a; // <- foo: { a: unknown }
}
```

And `fromAssertionFunction` takes an `AssertionFunction` and returns a `PredicateFunction`.

```ts
type Assert1 = (v: unknown) => asserts v is 1;
const assert1: Assert1 = (v: unknown): asserts v is 1 => {
  if (v !== 1) throw Error('');
};

const is1 = fromAssertionFunction(assert1);

declare const aaa: 1 | 2 | 3;
if (is1(aaa)) {
  // <- aaa: 1
} else {
  // <- aaa: 2 | 3
}
```

## Series Outro

It's the end, but don't close the article yet, I have some things to say.

This is the last article of our TypeScript narrowing series. The first series I did here and on YouTube.

I'm super happy with the quality that we were able to put out, but I also have big dreams. I want to make things _crazy_ better! And that's why me and my team are building a platform for interactive learning experiences.

Imagine consuming my content and in the middle of it there's a mini-game for you, or a 3D animation, or a quick quiz to consolidate your knowledge. You get the idea.

And all that, available in many languages. We currently offer our content in English and Portuguese. But I also want to offer it in Spanish, German, French, and so many others!

For now, we're releasing all that content for free, but I think it's obvious to say that we'll eventually have paid courses, and I want them to be f\** awesome! Like, deliver *unbelievable\* value!

<p>So sure, if you haven't yet, I highly encourage you to <app-newsletter-modal-trigger>subscribe to the newsletter</app-newsletter-modal-trigger>. Your support is highly appreciated.</p>

Thank you _so_ much for sticking with me, I hope you enjoyed it, and I hope this is only the beginning of a long journey on YouTube and content creation in general. 🙂

## Conclusion

As always, references are below.

And if your company is looking for remote web developers, please consider contacting me and my team on [lucaspaganini.com](https://lucaspaganini.com/).

Until then, have a great day, and I’ll see you in the next one.

## Related Content

1. TypeScript Narrowing pt. 1 - 8

## References

1. [TypeScript Utilities Library - `@lucaspaganini/ts`_Github Repository_](https://github.com/LucasPaganini/ts)