@interaktiv/types
v1.1.0
Published
Types and related tools for Javascript at DIA
Downloads
15
Readme
@interaktiv/types
Types and related tools for Javascript at DIA
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 Javascript libraries, applications, and plugins consisting of "two" parts:
- A collection of type-narrowing convenience functions for writing concise type-guards.
- Maybe in the future a collection of commonly desired types for TypeScript.
Table of Contents
Installation
This module can be installed via npm which is bundled with Node.js and should be installed as one of your project's dependencies:
npm install --save @interaktiv/types
Usage
For example, look at the following typical untyped JSON processing in 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:
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!
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:
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.
// 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.
// 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.
// 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).
// 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.
// 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.
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.
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.
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 before contributing.
Acknowledgements
This library is heavily inspired by @salesforce/ts-types. Thank you 💙
License
MIT Copyright © 2019-present die.interaktiven GmbH & Co. KG