npm package: @interaktiv/types
Description: Types and related tools for Javascript at DIA
Author: die.interaktiven GmbH & Co. KG
Latest version: 1.1.0

# @interaktiv/types

> Types and related tools for Javascript at [DIA][dia-website]

[![Commitizen friendly][commitizen-badge]][commitizen]
[![Conventional Commits][conventional-commits-badge]][conventional-commits]
[![Semantic Release][semantic-release-badge]][semantic-release]
[![Code of Conduct][coc-badge]][coc] [![MIT License][license-badge]][license]

[![npm latest version][latest-version-badge]][package]
[![npm next version][next-version-badge]][package]

## The Problem

Using type guards in your code improves its runtime type safety characteristics,
makes it more readable, and provides richer typing information for IDEs. Type
guards are implemented as conditional statements, however, and can quickly
become noisy and make what was once terse JavaScript code expand into several
lines of type checking.

This library aimed to simplify the experience of reducing the amount of type
guards needed to process e.g. a typed-JSON data structure by providing several
convenience functions that help extract well-typed data from such JSON
structures.

## This Solution

This is a simple library developed for use in [DIA][dia-website] Javascript
libraries, applications, and plugins consisting of "two" parts:

1. A collection of type-narrowing convenience functions for writing concise
   type-guards.
1. Maybe in the **future** a collection of commonly desired types for
   TypeScript.

## Table of Contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Installation](#markdown-header-installation)
- [Usage](#markdown-header-usage)
  - [Narrowing functions](#markdown-header-narrowing-functions)
- [References](#markdown-header-references)
- [Other Use Cases](#markdown-header-other-use-cases)
- [Acknowledgements](#markdown-header-acknowledgements)
- [License](#markdown-header-license)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Installation

This module can be installed via [npm][npm-cli] which is bundled with
[Node.js][node] and should be installed as one of your project's
[dependencies][]:

```bash
npm install --save @interaktiv/types
```

## Usage

For example, look at the following typical untyped JSON processing in
JavaScript:

```javascript
// concise, but not at all null-safe or type-safe
// often made to be at least null-safe using lodash functions
JSON.parse(response.body).results.forEach(item => db.save(item.id, item));
```

Then a safe version in bare TypeScript using type guards:

```javascript
const json = JSON.parse(response.body);
// type of json -> `any`, but will not be undefined or JSON.parse would throw
if (json === null && typeof json !== 'object')
  throw new Error('Unexpected json data type');
let results = json.results;

// type of results -> `any`
if (!Array.isArray(results)) results = [];

// type of results -> `any[]`
results.forEach(item => {
  // type of item -> `any`
  const id = item.id;

  // type of id -> `any`
  if (typeof id !== 'string') throw new Error('Unexpected item id data type');

  // type of id -> `string`
  db.save(id, item);
});
```

While that's pretty safe, it's also a mess to read and write. That's why this
library is here to help!

```javascript
const json = ensureJsonMap(JSON.parse(response.body));

// type of json -> `JsonMap` or raises an error
const results = asJsonArray(json.results, []);

// type of results -> `JsonArray` or uses the default of `[]`
results.forEach(item => {
  // type of item -> `AnyJson`
  record = ensureJsonMap(record);
  db.save(ensureString(record.id), record);
});
```

Removing the comments, we can create something short with robust type and null
checking implemented:

```javascript
asJsonArray(ensureJsonMap(JSON.parse(response.body)).results, []).forEach(
  item => {
    const record = ensureJsonMap(item);
    db.save(ensureString(record.id), record);
  },
);
```

The `ensure*` functions are used in this example since they will raise an error
when the value being checked either does not exist or does not match the
expected type. Of course, you don't always want to raise an error when these
conditions are not met, so alternative forms exist for each of the JSON data
types that allow the types to be tested and narrowed -- see the `is*` and `as*`
variants for testing and narrowing capabilities without additionally raising
errors.

### Narrowing functions

This library provides several categories of functions to help with safely
narrowing variables of broadly typed variables, like `unknown` or `object`, to
more specific types.

#### is\*

The `is*` suite of functions accept a variable of a broad type such as `unknown`
or `object` and returns a `boolean` type-predicate useful for narrowing the type
in conditional scopes.

```javascript
// type of value -> string | boolean
if (isString(value)) {
  // type of value -> string
}
// type of value -> boolean
```

#### as\*

The `as*` suite of functions accept a variable of a broad type such as `unknown`
or `object` and optionally returns a narrowed type after validating it with a
runtime test. If the test is negative or if the value was not defined (i.e.
`undefined` or `null`), `undefined` is returned instead.

```javascript
// some function that takes a string or undefined
function upperFirst(s) {
  return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
}
// type of value -> unknown
const name = upperFirst(asString(value));
// type of name -> Optional<string>
```

#### ensure\*

The `ensure*` suite of functions narrow values' types to a definite value of the
designated type, or raises an error if the value is `undefined` or of an
incompatible type.

```javascript
// type of value -> unknown
try {
  const s = ensureString(value);
  // type of s -> string
} catch (err) {
  // s was undefined, null, or not of type string
}
```

#### has\*

The `has*` suite of functions both tests for the existence and
type-compatibility of a given value and, if the runtime value check succeeds,
narrows the type to a view of the original value's type intersected with the
tested property (e.g. `T & { [_ in K]: V }` where `K` is the test property key
and `V` is the test property value type).

```javascript
// type of value -> unknown
if (hasString(value, 'name')) {
  // type of value -> { name: string }
  // value can be further narrowed with additional checks
  if (hasArray(value, 'results')) {
    // type of value -> { name: string } & { results: unknown[] }
  } else if (hasInstance(value, 'error', Error)) {
    // type of value -> { name: string } & { error: Error }
  }
}
```

#### get\*

The `get*` suite of functions search an `unknown` target value for a given path.
Search paths follow the same syntax as `lodash`'s `get`, `set`, `at`, etc. These
functions are more strictly typed, however, increasingly the likelihood that
well-typed code stays well-typed as a function's control flow advances.

```javascript
// imagine response json retrieved from a remote query
const response = {
  start: 0,
  length: 2,
  results: [{ name: 'first' }, { name: 'second' }],
};
const nameOfFirst = getString(response, 'results[0].name');
// type of nameOfFirst = string
```

#### coerce\*

The `coerce` suite of functions accept values of general types and narrow their
types to JSON-specific values. They are named with the `coerce` prefix to
indicate that they do not perform an exhaustive runtime check of the entire data
structure -- only shallow type checks are performed. As a result, _only_ use
these functions when you are confident that the broadly typed subject being
coerced was derived from a JSON-compatible value.

```javascript
const response = coerceJsonMap(
  JSON.parse(await http.get('http://example.com/data.json').body),
);
// type of response -> JsonMap
```

#### Object Utilities

This suite of functions are used to iterate the keys, entries, and values of
objects with some typing conveniences applied that are not present in their
built-in counterparts (i.e. `Object.keys`, `Object.entries`, and
`Object.values`), but come with some caveats noted in their documentation.
Typical uses include iterating over the properties of an object with more useful
`keyof` typings applied during the iterator bodies, and/or filtering out
`undefined` or `null` values before invoking the iterator functions.

```javascript
const pets = {
  fido: 'dog',
  bill: 'cat',
  fred: undefined,
};

// note that the array is typed as [string, string] rather than [string, string | undefined]
function logPet([name, type]: [string, string]) {
  console.log('%s is a %s', name, type);
}

definiteEntriesOf(pets).forEach(logPet);
// fido is a dog
// bill is a cat
```

## References

This library is using custom error types from
[@interaktiv/errors][interaktiv-errors].

## Other Use Cases

If you lack some use cases, you are welcome to open a pull request and add it.
We'll come back to you and see how we can support your use case and present it
to all devs.

Please consult the [contribution guides][contributing] before contributing.

## Acknowledgements

This library is heavily inspired by [@salesforce/ts-types][salesforce-ts-types].
Thank you 💙

## License

[MIT][license] Copyright © 2019-present [die.interaktiven GmbH & Co.
KG][dia-website]

[coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg
[coc]: ./other/CODE_OF_CONDUCT.md
[commitizen]: http://commitizen.github.io/cz-cli
[commitizen-badge]:
  https://img.shields.io/badge/commitizen-friendly-brightgreen.svg
[contributing]: ./CONTRIBUTING.md
[conventional-commits]: https://conventionalcommits.org
[conventional-commits-badge]:
  https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg
[dependencies]:
  https://docs.npmjs.com/specifying-dependencies-and-devdependencies-in-a-package-json-file
[dia-website]: https://die-interaktiven.de
[downloads-badge]: https://img.shields.io/npm/dw/@interaktiv/types.svg
[downloads-total-badge]: https://img.shields.io/npm/dt/@interaktiv/types.svg
[interaktiv-errors]: https://www.npmjs.com/package/@interaktiv/errors
[latest-version-badge]:
  https://img.shields.io/npm/v/@interaktiv/types/latest.svg
[license]: https://opensource.org/licenses/MIT
[license-badge]: https://img.shields.io/npm/l/@interaktiv/types.svg
[next-version-badge]: https://img.shields.io/npm/v/@interaktiv/types/next.svg
[package]: https://npmjs.com/package/@interaktiv/types
[node]: https://nodejs.org
[npm]: https://www.npmjs.com
[npm-cli]: https://www.npmjs.com/package/npm
[salesforce-ts-types]: https://www.npmjs.com/package/@salesforce/ts-types
[semantic-release]: https://github.com/semantic-release/semantic-release
[semantic-release-badge]:
  https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
[version-badge]: https://img.shields.io/npm/v/@interaktiv/types.svg