Skip to content

Generic type declarations for e.g. TypeScript and JSON Schema

License

Notifications You must be signed in to change notification settings

grantila/core-types

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

41 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

npm version downloads build status coverage status Node.JS version

core-types

This package provides TypeScript types describing core types useful in TypeScript, JavaScript, JSON, JSON Schema etc. It also contains functions for simplifying unnecessarily complex types, as well helper utilities for other packages converting to/from core-types and another type system.

Using core-types, e.g. implementing conversions to other type systems is easy, since core-types is relatively small and well defined.

  • See use cases and other packages using this package
  • Usage
  • Specification of the types in this package are:
    • any (any of the below types)
    • null
    • boolean (true, false)
    • string
    • number and integer; distinguished in JSON Schema, equivalent in TypeScript
    • object; key-value of core types where the key is a string
    • array; list of arbitrary length of a specific core type
    • tuple; list of specific length with distinguished core types in each position
    • ref a reference to a named type
    • unions and intersections of the above

The above describes JSON completely, and is a lowest common denominator for describing types useful for JSON, JSON Schema and TypeScript. Think of it as an extremely simplified version of JSON Schema.

See

This package is used by:

Versions

Since 3.0 this package is pure ESM and requires Node 14.13.1 or later.

Usage

To create a core-types type, just cast it to NodeType.

import type { NodeType } from 'core-types'

const myStringType: NodeType = { type: 'string' };

For more information on the specific types, see the Specification.

simplify

The function simplify can take a type, or an array of types, and returns simplified type definitions.

Examples of simpifications performed:

  • An empty and or or will often be removed.
  • A union (e.g. of any and string), except for usages const and enum, can be simplified as just any.
  • An intersection of any and string can be simplified to string.
  • An or containing child ors, will be flattened to the parent or.
  • and more...

The simplify function is type-wise lossless, but can remove annotations (e.g. descriptions). It is however usually recommended to perform a simplification after a type has been converted to core-types before converting to another type system.

import { simplify } from 'core-types'

const simplified = simplify( myType );

simplify( {
	type: 'or',
	or: [
		{ type: 'or', or: [ { type: 'string' } ] },
		{ type: 'any', const: 'foo' }
	]
} ); // { type: 'string', const: 'foo' }

validate

The validate function validates that a NodeType type tree is valid.

It ensures e.g.

  • Non-negative integer minItems
  • Non-mismatching enums and const if both are specified.
import { validate } from 'core-types'

validate( myType ); // Throws error if not valid

traverse

The traverse function traverses a type tree and calls a callback function for every node it finds.

The callback function gets an object as argument on the following form:

interface TraverseCallbackArgument
{
	node: NodeType;
	rootNode: NodeType;
	path: Array< string | number >;
	parentProperty?: string;
	parentNode?: NodeType;
	index?: string | number;
	required?: boolean;
}
import { traverse } from 'core-types'

traverse( rootNode, ( { node } ) => {
	if ( !node.title )
		node.title = "This is a dummy title";
} );

some

The some function is similar to traverse but the callback can return a boolean. If the callback returns true, some returns true, otherwise false.

This is useful to quickly find if a node satisifes a certain criteria, and is similar to Array.prototype.some.

import { some } from 'core-types'

const hasRefNode = some( rootNode, ( { node: { type } } ) => type === 'ref' );

helpers

When implementing conversions to and from core-types, the following helper functions may come in handy:

  • ensureArray converts values to arrays of such values, or returns arrays as-is. null and undefined become empty array
  • isPrimitiveType returns true for primitive NodeTypes
  • hasConstEnum returns true for NodeTypes which has (or can have) const and enum properties.
  • isEqual deep-equal comparison (of JSON compatible non-recursive types)
  • intersection returns an array of values found in both of two arrays. Handles primitives as well as arrays and objects (uses isEqual)
  • union returns an array of unique values from two arrays. Handles primitives as well as arrays and objects (uses isEqual)
  • isNonNullable
  • isCoreTypesError
  • decorateErrorMeta
  • decorateError
  • getPositionOffset
  • mergeLocations

Annotations

  • mergeAnnotations
  • extractAnnotations
  • stringifyAnnotations
  • stripAnnotations
  • stringify

Conversion

When converting, a conversion package is recommended to return a ConversionResult<T>, i.e. the data as property data in an object which also contains information about the conversion:

interface ConversionResult< T = string >
{
	data: T;
	convertedTypes: Array< string >;
	notConvertedTypes: Array< string >;
}

Specification

The main type is called NodeType and is a union of the specific types. A NodeType always has a type property of the type Types. The Types is defined as:

type Types =
	| 'any'
	| 'null'
	| 'boolean'
	| 'string'
	| 'number'
	| 'integer'
	| 'object'
	| 'array'
	| 'tuple'
	| 'ref'
	| 'and'
	| 'or';

Depending on which type is used, other properties in NodeType will be required. In fact, the NodeType is defined as:

type NodeType =
	| AnyType
	| NullType
	| BooleanType
	| StringType
	| NumberType
	| IntegerType
	| ObjectType
	| ArrayType
	| TupleType
	| RefType
	| AndType
	| OrType;

These types have an optional name (string) property which can be converted to be required using NamedType<T = NodeType>. This is useful when converting to other type systems where at least the top-most types must have names (like JSON Schema definitions or exported TypeScript types/interfaces), and is used by the NodeDocument, which is what conversion packages should use:

interface NodeDocument
{
	version: 1; // core-types only has version 1 so far
	types: Array< NamedType >;
}

The types also have optional annotation properties title (string), description (string), examples (string or array of strings), default (string), see (string or array of strings) and comment (string).

All types except NullType, AndType and OrType can have two properties const (of type T) or enum (of type Array<T>). The T depends on the NodeType. These have the same semantics as in JSON Schema, meaning a const value is equivalent of an enum with only that value. The enum can be seen as a type literal union in TypeScript.

any type

The AnyType matches any type. Its const and enum properties have the element type T set to unknown.

This corresponds to any or unknown in TypeScript, and the empty schema {} in JSON Schema.

Example: { type: 'any' }

null type

The NullType is simply equivalent to the TypeScript, JavaScript and JSON type null.

Example: { type: 'null' }

boolean type

The BooleanType is equivalent to the TypeScript, JavaScript and JSON Boolean (true and false).

The element type T for const and enum is boolean.

Example: { type: 'boolean', const: false }

string type

The StringType is equivalent to the TypeScript, JavaScript and JSON type String.

The element type T for const and enum is string.

Example: { type: 'string', enum: [ "foo", "bar" ] }

number type

core-types distinguishes between NumberType and IntegerType.

In TypeScript, JavaScript and JSON they are both equivalent to Number. In JSON Schema however, integer is a separate type, and can therefore be converted to core-types with maintained type information.

The element type T for const and enum is number.

Example: { type: 'number', enum: [ 17, 42 ] }

object type

The ObjectType is used to describe the TypeScript type Record<string, NodeType> and the JavaScript and JSON type Object. In TypeScript or JavaScript, the keys must only be strings, not numbers or symbols.

The element type T for const and enum is Record<string, any-json-type>, i.e. plain objects.

Two more properties are required for an ObjectType, properties and additionalProperties.

properties is defined as Record<string, { node: NodeType; required: boolean; }>.

additionalProperties is defined as boolean | NodeType. When this is false, no additional properties apart from those defined in properties are allowed, and if true properties are allowed of any type (AnyType). Otherwise additional properties are allowed of the defined NodeType.

Example:

{
	type: 'object',
	properties: {
		name: { node: { type: 'string' }, required: true },
		age: { node: { type: 'number' }, required: true },
		level: { node: { type: 'string', enum: [ 'novice', 'proficient', 'expert' ] }, required: false },
	},
	additionalProperties: false,
}

array type

The ArrayType is used to describe the TypeScript type Array<NodeType> and the JavaScript and JSON type Array.

The element type T for const and enum is Array<any-json-type>, i.e. arrays of JSON-compatible types defined by the NodeType in elementType.

The extra and required property elementType is of type NodeType and defines what types the array can hold.

Example:

{
	type: 'array',
	elementType: { type: 'string' },
}

tuple type

The TupleType describes specific-length arrays where each position has a specific type. It matches the tuple type [A, B, ...] in TypeScript and is an Array in JavaScript and JSON.

The element type T for const and enum is [...any-json-types], i.e. tuples of JSON-compatible types defined by the NodeType in the required elementTypes and additionalItems.

The extra and required properties for TupleType are elementTypes, minItems and additionalItems.

elementTypes is defined as [...NodeType] and describes the valid types for each position in the tuple for the required and individually typed optional tuple elements.

minItems is an integer (TypeScript/JavaScript number) defining the minimum required elements and must not be negative. If this is greater than the number of elementTypes, although valid in core-types per se, some conversions will limit it to the size of elementTypes.

additionalProperties is used to describe optional extra elemenents. It is defined as boolean | NodeType. When this is false, no additional elements are allowed, and if true elements are allowed of any type (AnyType). Otherwise additional elemenets are allowed of the defined NodeType.

Example:

{
	type: 'tuple',
	elementTypes: [
		{ type: 'string' },
		{ type: 'boolean' }, // Optional, because minItems is 1
	],
	minItems: 1,
	additionalItems: { type: 'number' },
}

ref type

The RefType describes references to other named types. Exactly what this means is up to the implementation of the user of core-types, but it is recommended that a reference type in a list of NodeTypes refers to a named type within that list. This corresponds to TypeScript named types being referred to in the same file as in which the type is defined, or JSON Schema $ref references only referring to #/definitions/* types.

A RefType has a required property ref which is a string corresponding to the name of the reference.

Example:

[
	{
		name: 'User',
		type: 'object',
		properties: {
			name: { node: { type: 'string' }, required: true },
			id: { node: { type: 'number' }, required: true },
		},
		additionalProperties: true,
	},
	{
		name: 'UserList',
		type: 'array',
		elementType: { type: 'ref', ref: 'User' },
	},
]

union type

The OrType describes a union of other types. This is equivalent to union types in TypeScript (e.g. number | string) and anyOf in JSON Schema.

An OrType has a required property or which is defined as Array<NodeType>.

Example:

{
	type: 'or',
	or: [
		{ type: 'string' },
		{ type: 'number' },
		{ type: 'ref', ref: 'IdType' }, // Defined somewhere...
	},
}

intersection type

The AndType describes an intersection of other types. This is equivalent to intersection types in TypeScript (e.g. A & B) and allOf in JSON Schema.

An AndType has a required property and which is defined as Array<NodeType>.

Example:

[
	{
		name: 'CommentWithId',
		type: 'and',
		and: [
			{ type: 'ref', ref: 'Comment' },
			{ type: 'ref', ref: 'WithId' },
		},
	},
	{
		name: 'Comment',
		type: 'object',
		properties: {
			line: { node: { type: 'string' }, required: true },
			user: { node: { type: 'ref', ref: 'User' }, required: true },
		},
		additionalProperties: false,
	},
	{
		name: 'WithId',
		type: 'object',
		properties: {
			id: { node: { type: 'string' }, required: true },
		},
		additionalProperties: false,
	},
]

About

Generic type declarations for e.g. TypeScript and JSON Schema

Resources

License

Stars

Watchers

Forks

Packages

No packages published