This is a simple TypeScript-oriented library developed for use in Salesforce CLI libraries, applications, and plugins consisting of two parts:
We were interested in enabling strict compiler settings in TypeScript. Among the sub-settings that comprise strict mode are "strict null checks", "strict property initialization", and "no implicit any". These settings have the potential to increase code quality substantially, reducing the frequency of certain classes of runtime errors typical in JavaScript applications. They also encourage the writing of clearer code, which helps teams work together and more rapidly onboard new hires.
Of course, stricter compiler settings require developers to write more type-safe code -- or to work around the compiler's insistence on type-safety. Often this stricter style leads to more verbose code in the way of type declarations and type guards, and can require new and occasionally unfamiliar patterns to accomplish without subverting the compiler's enforcement of the type system (typically via the use of type assertions).
TypeScript provides both syntax and built-in types designed to help write well-typed code, but we can be more terse and concise by employing additional types and type-oriented utilities. That's where this library comes in.
This library has its roots in solving the problem of how to handle untyped JSON data in a type-safe way. It was born when we added some basic type declarations to replace the unsafe any type as a stand-in for JSON data with a type that could capture the range of data available in any JSON construct. This yielded the AnyJson type, which is effectively a union of all primitive and collection JSON values. The type alone was not enough to make for convenient, type-guarded handling of JSON data, however. TypeScript supports a very elegant system of control flow analysis that will narrow the type of a variable in code after the compiler can prove the set of possible types of the variable has been reduced. 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 a typed-JSON data structure by providing several convenience functions that help extract well-typed data from such JSON structures.
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 fns
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 shorten the above somewhat to achieve something not much more complex than the original example, but 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. Additionally, and perhaps more importantly, the generic any and AnyJson types get progressively narrowed when using these functions to more specific types. 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 in the API documentation for testing and narrowing capabilities without additionally raising errors.
After a few iterations of working on the JSON support types and utilities, it became apparent that we needed other non-JSON types and functions that provide similar capabilities. Rather than create a new library for those, we instead grew the scope of this one to contain all of our commonly used types and narrowing functions.
Another Salesforce TypeScript library, @salesforce/kit, builds on this library to add additional utilities. It includes additional JSON support, a lightweight replacement for lodash, and growing support for patterns used in other Salesforce CLI libraries and applications.
Any function returning type T. T defaults to unknown when not explicitly supplied.
Any valid JSON value.
Any valid JSON collection value.
Any valid JSON primitive value.
An alias for the commonly needed Extract<keyof T, string>.
An alias for a tuple of type [string, T]' for a given generic typeT.Tdefaults tounknown` if not otherwise
defined.
Extracts literally defined property names from a type T as a union of key name strings, minus
any index signatures.
Creates a new Record type from the literal properties of a type T, assigning their values
the to the type U.
This can be useful for creating interfaces from the keys of an enum so that the keys are
available at runtime for meta-programming purposes, while both tying the properties of the
generated type to the enum keys and remaining as DRY as possible.
enum QUERY_KEY { id, name, created, updated }
// typeof QUERY_KEY -> {
// [x: number]: number;
// readonly id: number;
// readonly name: number;
// readonly created: number;
// readonly updated: number;
// }
interface QueryRecord extends LiteralsRecord<typeof QUERY_KEY, string> { }
// typeof QueryRecord -> {
// readonly id: string;
// readonly name: string;
// readonly created: string;
// readonly updated: string;
// }
// And for an interface with writable properties, use the following:
interface QueryRecord extends Writable<LiteralsRecord<typeof QUERY_KEY, string>> { }
// typeof QueryRecord -> {
// id: string;
// name: string;
// created: string;
// updated: string;
// }
A union type for either the parameterized type T or an array of T.
Subtracts undefined from any union type T. This is the opposite of Optional.
A union type for either the parameterized type T, null, or undefined -- the opposite of
the NonNullable builtin conditional type.
Creates a new type that omits keys in union type K of a target type T.
A union type for either the parameterized type T or undefined -- the opposite of NonOptional.
A union type of string | number | symbol, representing all possible types for object property keys.
Converts a type T that may have optional, nullable properties into a new type with only required
properties, while also subtracting null from all possible property values.
type Foo = { bar?: string | undefined | null };
type RequiredNonNullableFoo = RequiredNonNullable<Foo>;
// RequiredNonNullableFoo -> { bar: string };
Converts a type T that may have optional properties into a type T with only required
properties (e.g. undefined values are not allowed). Explicit nulls in value unions
will still be possible. This is similar to the Required builtin mapped type, but also
subtracts undefined from value union types as well as the optional property declaration.
type Foo = { bar?: string | undefined | null };
type RequiredNonOptionalFoo = RequiredNonOptional<Foo>;
// RequiredNonOptionalFoo -> { bar: string | null };
Converts readonly properties of a type T to writable properties. This is the opposite of the
Readonly<T> builtin mapped type.
Narrows an AnyJson value to a boolean if it is type-compatible, or returns undefined otherwise.
Narrows an unknown value to a boolean if it is type-compatible, or returns the provided default otherwise.
The value to test.
The default to return if value was undefined or of the incorrect type.
Narrows an AnyJson value to a JsonArray if it is type-compatible, or returns undefined otherwise.
Narrows an unknown value to a JsonArray if it is type-compatible, or returns the provided default otherwise.
The value to test.
The default to return if the value was undefined or of the incorrect type.
Narrows an AnyJson value to a JsonMap if it is type-compatible, or returns undefined otherwise.
Narrows an unknown value to a JsonMap if it is type-compatible, or returns the provided default otherwise.
The value to test.
The default to return if value was undefined or of the incorrect type.
Narrows an AnyJson value to a number if it is type-compatible, or returns undefined otherwise.
Narrows an unknown value to a number if it is type-compatible, or returns the provided default otherwise.
The value to test.
The default to return if value was undefined or of the incorrect type.
Narrows an AnyJson value to a string if it is type-compatible, or returns undefined otherwise.
Narrows an unknown value to a string if it is type-compatible, or returns the provided default otherwise.
The value to test.
The default to return if value was undefined or of the incorrect type.
Narrows an unknown value to an AnyJson if it is type-compatible*, or returns undefined otherwise.
* This is not a 100% safe operation -- it will not deeply validate plain object or array structures
to ensure that they contain only AnyJson values. When type-narrowing potential objects or arrays with this
function, it's the responsibility of the caller to understand the risks of making such a shallow type assertion
over the value data.
The value to test.
Narrows an unknown value to an AnyJson if it is type-compatible, or returns the provided default otherwise.
The value to test.
The default to return if value was undefined or of the incorrect type.
Narrows an array of type T to a JsonArray using a shallow type-compatibility check. Use this when the source of
the array is known to be JSON-compatible and you want simple type coercion to a JsonArray. Use toJsonArray
instead when the value array cannot be guaranteed to be JSON-compatible and you want an assurance of runtime
type safety. This is a shortcut for writing asJsonArray(coerceAnyJson(value)).
The array to coerce.
Narrows an array of type T to a JsonArray using a shallow type-compatibility check. Use this when the source of
the array is known to be JSON-compatible and you want simple type coercion to a JsonArray. Use toJsonArray
instead when the value array cannot be guaranteed to be JSON-compatible and you want an assurance of runtime
type safety.
The array to coerce.
The default to return if value was undefined.
Narrows an object of type T to a JsonMap using a shallow type-compatibility check. Use this when the source of
the object is known to be JSON-compatible and you want simple type coercion to a JsonMap. Use toJsonMap
instead when the value object cannot be guaranteed to be JSON-compatible and you want an assurance of runtime
type safety. This is a shortcut for writing asJsonMap(coerceAnyJson(value)).
The object to coerce.
Narrows an object of type T to a JsonMap using a shallow type-compatibility check. Use this when the source of
the object is known to be JSON-compatible and you want simple type coercion to a JsonMap. Use toJsonMap
instead when the value object cannot be guaranteed to be JSON-compatible and you want an assurance of runtime
type safety.
The object to coerce.
The default to return if value was undefined.
Returns an array of all entry tuples of type [K, NonNullable<T[K]>] in an object T whose values are neither
null nor undefined. This can be convenient for enumerating the entries of unknown object with optional properties
(including Dictionarys) without worrying about performing checks against possibly undefined or null values.
See also caveats outlined in entriesOf.
The object of interest.
Returns an array of all string keys in an object of type T whose values are neither null nor undefined.
This can be convenient for enumerating the keys of definitely assigned properties in an object or Dictionary.
See also caveats outlined in keysOf.
The object of interest.
Returns an array of all values of type T in a Dictionary<T> for values that are neither null nor undefined.
This can be convenient for enumerating all non-nullable values of unknown Dictionary.
The object of interest.
Narrows a type Nullable<T> to a T or raises an error.
The value to test.
The error message to use if value is undefined or null.
Returns the entries of an object of type T. This is like Object.entries except the return type
captures the known keys and value types of T.
Note that it is the responsibility of the caller to use this wisely -- there are cases where
the runtime set of entries returned may be broader than the type checked set at compile time,
so there's potential for this to be abused in ways that are not inherently type safe. For
example, given base class Animal, subclass Fish, and const animal: Animal = new Fish();
then entriesOf(animal) will not type-check the entire set of keys of the object animal since
it is actually an instance of type Fish, which has an extended property set.
In general, it should be both convenient and type-safe to use this when enumerating the entries of simple data objects with known properties.
interface Point { x: number; y: number; }
const point: Point = { x: 1, y: 2 };
// type of entries -> ['x' | 'y', number][]
const entries = entriesOf(point);
for (const entry of entries) {
console.log(entry[0], entry[1]);
}
// x 1
// y 2
The object of interest.
Gets AnyJson element of a JsonMap given a query path.
Gets an AnyJson element of a JsonMap given a query path, returning a default if not found or not type-compatible.
The JsonMap to query.
The query path.
A fallback value.
Gets a boolean element of a JsonMap given a query path.
Gets a boolean element of a JsonMap given a query path, returning a default if not found or not type-compatible.
The JsonMap to query.
The query path.
A fallback value.
Gets a JsonArray element of a JsonMap given a query path.
Gets a JsonArray element of a JsonMap given a query path, returning a default if not found or not type-compatible.
The JsonMap to query.
The query path.
A fallback value.
Gets a JsonMap element of a JsonMap given a query path.
Gets a JsonMap element of a JsonMap given a query path, returning a default if not found or not type-compatible.
The JsonMap to query.
The query path.
A fallback value.
Gets a number element of a JsonMap given a query path.
Gets a number element of a JsonMap given a query path, returning a default if not found or not type-compatible.
The JsonMap to query.
The query path.
A fallback value.
Gets a string element of a JsonMap given a query path.
Gets a string element of a JsonMap given a query path, returning a default if not found or not type-compatible.
The JsonMap to query.
The query path.
A fallback value.
Gets an AnyJson element of a JsonMap given a query path, or raises an error if not found or not type-compatible.
The JsonMap to query.
The query path.
The error message to use if value is not type-compatible.
Gets a boolean element of a JsonMap given a query path, or raises an error if not found or not type-compatible.
The JsonMap to query.
The query path.
The error message to use if value is not type-compatible.
Gets a JsonArray element of a JsonMap given a query path, or raises an error if not found or not type-compatible.
The JsonMap to query.
The query path.
The error message to use if value is not type-compatible.
Gets a JsonMap element of a JsonMap given a query path, or raises an error if not found or not type-compatible.
The JsonMap to query.
The query path.
The error message to use if value is not type-compatible.
Gets a number element of a JsonMap given a query path, or raises an error if not found or not type-compatible.
The JsonMap to query.
The query path.
The error message to use if value is not type-compatible.
Gets a string element of a JsonMap given a query path, or raises an error if not found or not type-compatible.
The JsonMap to query.
The query path.
The error message to use if value is not type-compatible.
Tests whether an unknown value is an Array.
Any value to test.
Tests whether an unknown value is a boolean.
Any value to test.
Tests whether an unknown value is a function.
Any value to test.
Tests whether or not a key string is a key of the given object type T.
The string to test as a key of the target object.
The target object to check the key in.
Tests whether an unknown value is a number.
Any value to test.
Tests whether an unknown value is an object subtype.
Any value to test.
Tests whether or not an unknown value is a plain JS object.
Any value to test.
Tests whether an unknown value is a string.
Any value to test.
Returns the keys of an object of type T. This is like Object.keys except the return type
captures the known keys of T.
Note that it is the responsibility of the caller to use this wisely -- there are cases where
the runtime set of keys returned may be broader than the type checked set at compile time,
so there's potential for this to be abused in ways that are not inherently type safe. For
example, given base class Animal, subclass Fish, and const animal: Animal = new Fish();
then keysOf(animal) will not type-check the entire set of keys of the object animal since
it is actually an instance of type Fish, which has an extended property set.
In general, it should be both convenient and type-safe to use this when enumerating the keys of simple data objects with known properties.
interface Point { x: number; y: number; }
const point: Point = { x: 1, y: 2 };
const keys = keysOf(point);
// type of keys -> ('a' | 'b')[]
for (const key of keys) {
console.log(key, point[key]);
}
// x 1
// y 2
The object of interest.
Narrows an object of type T to an AnyJson following a deep, brute-force conversion of the object's data to
only consist of JSON-compatible values by performing a basic JSON clone on the object. This is preferable to
using the weaker coerceAnyJson(unknown) to type-narrow an arbitrary value to an AnyJson when the value's source
is unknown, but it comes with the increased overhead of performing the deep JSON clone to ensure runtime type
safety. The use of JSON cloning guarantees type safety by omitting non-JSON-compatible elements from the resulting
JSON data structure. Use coerceAnyJson(unknown) when the value object can be guaranteed to be JSON-compatible
and only needs type narrowing.
The value to convert.
Narrows an object of type T to an AnyJson following a deep, brute-force conversion of the object's data to
only consist of JSON-compatible values by performing a basic JSON clone on the object. This is preferable to
using the weaker coerceAnyJson(unknown) to type-narrow an arbitrary value to an AnyJson when the value's source
is unknown, but it comes with the increased overhead of performing the deep JSON clone to ensure runtime type
safety. The use of JSON cloning guarantees type safety by omitting non-JSON-compatible elements from the resulting
JSON data structure. Use coerceAnyJson(unknown) when the value object can be guaranteed to be JSON-compatible
and only needs type narrowing.
The value to convert.
The default to return if value was undefined.
Narrows an array of type T to a JsonArray following a deep, brute-force conversion of the array's data to
only consist of JSON-compatible values by performing a basic JSON clone on the array. This is preferable to
using the weaker coerceJsonArray(array) to type-narrow an arbitrary array to a JsonArray when the array's source
is unknown, but it comes with the increased overhead of performing the deep JSON clone to ensure runtime type
safety. The use of JSON cloning guarantees type safety by omitting non-JSON-compatible elements from the resulting
JSON data structure. Non-JSON entries will be converted to nulls. Use coerceJsonArray(array) when the value
object can be guaranteed to be JSON-compatible and only needs type narrowing.
The array to convert.
Narrows an object of type T to a JsonMap following a deep, brute-force conversion of the object's data to
only consist of JSON-compatible values by performing a basic JSON clone on the object. This is preferable to
using the weaker coerceJsonMap(object) to type-narrow an arbitrary array to a JsonMap when the object's source
is unknown, but it comes with the increased overhead of performing the deep JSON clone to ensure runtime type
safety. The use of JSON cloning guarantees type safety by omitting non-JSON-compatible elements from the resulting
JSON data structure. Non-JSON entries will be converted to nulls. Use coerceJsonArray(array) when the value
object can be guaranteed to be JSON-compatible and only needs type narrowing.
The array to convert.
The default to return if the value was undefined or of the incorrect type.
Narrows an object of type T to a JsonMap following a deep, brute-force conversion of the object's data to
only consist of JSON-compatible values by performing a basic JSON clone on the object. This is preferable to
using the weaker coerceJsonMap(object) to type-narrow an arbitrary object to a JsonMap when the object's source
is unknown, but it comes with the increased overhead of performing the deep JSON clone to ensure runtime type
safety. The use of JSON cloning guarantees type safety by omitting non-JSON-compatible elements from the resulting
JSON data structure. Use coerceJsonMap(object) when the value object can be guaranteed to be JSON-compatible
and only needs type narrowing.
The object to convert.
Narrows an object of type T to a JsonMap following a deep, brute-force conversion of the object's data to
only consist of JSON-compatible values by performing a basic JSON clone on the object. This is preferable to
using the weaker coerceJsonMap(object) to type-narrow an arbitrary object to a JsonMap when the object's source
is unknown, but it comes with the increased overhead of performing the deep JSON clone to ensure runtime type
safety. The use of JSON cloning guarantees type safety by omitting non-JSON-compatible elements from the resulting
JSON data structure. Use coerceJsonMap(object) when the value object can be guaranteed to be JSON-compatible
and only needs type narrowing.
The object to convert.
The default to return if value was undefined.
Returns the values of an object of type T. This is like Object.values except the return type
captures the possible value types of T.
Note that it is the responsibility of the caller to use this wisely -- there are cases where
the runtime set of values returned may be broader than the type checked set at compile time,
so there's potential for this to be abused in ways that are not inherently type safe. For
example, given base class Animal, subclass Fish, and const animal: Animal = new Fish();
then valuesOf(animal) will not type-check the entire set of values of the object animal since
it is actually an instance of type Fish, which has an extended property set.
In general, it should be both convenient and type-safe to use this when enumerating the values of simple data objects with known properties.
interface Point { x: number; y: number; }
const point: Point = { x: 1, y: 2 };
const values = valuesOf(point);
// type of values -> number[]
for (const value of values) {
console.log(value);
}
// 1
// 2
The object of interest.
A constructor for any type
T.Tdefaults toobjectwhen not explicitly supplied.