Skip to content

Commit

Permalink
Add Jsonify type (#244)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
darcyparker and sindresorhus committed Aug 6, 2021
1 parent 7f63a6e commit 5349d27
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 1 deletion.
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export {Entries} from './source/entries';
export {SetReturnType} from './source/set-return-type';
export {Asyncify} from './source/asyncify';
export {Simplify} from './source/simplify';
export {Jsonify} from './source/jsonify';

// Template literal types
export {CamelCase} from './source/camel-case';
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ Click the type names for complete docs.
- [`Class`](source/basic.d.ts) - Matches a [`class`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes).
- [`Constructor`](source/basic.d.ts) - Matches a [`class` constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes).
- [`TypedArray`](source/typed-array.d.ts) - Matches any [typed array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray), like `Uint8Array` or `Float64Array`.
- [`JsonPrimitive`](source/basic.d.ts) - Matches a JSON primitive.
- [`JsonObject`](source/basic.d.ts) - Matches a JSON object.
- [`JsonArray`](source/basic.d.ts) - Matches a JSON array.
- [`JsonValue`](source/basic.d.ts) - Matches any valid JSON value.
Expand Down Expand Up @@ -122,6 +123,7 @@ Click the type names for complete docs.
- [`Asyncify`](source/asyncify.d.ts) - Create an async version of the given function type.
- [`Includes`](source/includes.ts) - Returns a boolean for whether the given array includes the given item.
- [`Simplify`](source/simplify.d.ts) - Flatten the type output to improve type hints shown in editors.
- [`Jsonify`](source/jsonify.d.ts) - Transform a type to one that is assignable to the `JsonValue` type.

### Template literal types

Expand Down
11 changes: 10 additions & 1 deletion source/basic.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,18 @@ Matches a JSON array.
*/
export type JsonArray = JsonValue[];

/**
Matches any valid JSON primitive value.
@category Basic
*/
export type JsonPrimitive = string | number | boolean | null;

/**
Matches any valid JSON value.
@see `Jsonify` if you need to transform a type to one that is assignable to `JsonValue`.
@category Basic
*/
export type JsonValue = string | number | boolean | null | JsonObject | JsonArray;
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
55 changes: 55 additions & 0 deletions source/jsonify.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {JsonPrimitive} from './basic';

// Note: The return value has to be `any` and not `unknown` so it can match `void`.
type NotJsonable = ((...args: any[]) => any) | undefined;

/**
Transform a type to one that is assignable to the `JsonValue` type.
@remarks
An interface cannot be structurally compared to `JsonValue` because an interface can be re-opened to add properties that may not be satisfy `JsonValue`.
@example
```
interface Geometry {
type: 'Point' | 'Polygon';
coordinates: [number, number];
}
const point: Geometry = {
type: 'Point',
coordinates: [1, 1]
};
const problemFn = (data: JsonValue) => {
// Does something with data
};
problemFn(point); // Error: type Geometry is not assignable to parameter of type JsonValue because it is an interface
const fixedFn = <T>(data: Jsonify<T>) => {
// Does something with data
};
fixedFn(point); // Good: point is assignable. Jsonify<T> transforms Geometry into value assignable to JsonValue
fixedFn(new Date()); // Error: As expected, Date is not assignable. Jsonify<T> cannot transforms Date into value assignable to JsonValue
```
@link https://github.com/Microsoft/TypeScript/issues/1897#issuecomment-710744173
@category Utilities
*/
type Jsonify<T> =
// Check if there are any non-JSONable types represented in the union.
// Note: The use of tuples in this first condition side-steps distributive conditional types
// (see https://github.com/microsoft/TypeScript/issues/29368#issuecomment-453529532)
[Extract<T, NotJsonable>] extends [never]
? T extends JsonPrimitive
? T // Primitive is acceptable
: T extends Array<infer U>
? Array<Jsonify<U>> // It's an array: recursive call for its children
: T extends object
? {[P in keyof T]: Jsonify<T[P]>} // It's an object: recursive call for its children
: never // Otherwise any other non-object is removed
: never; // Otherwise non-JSONable type union was found not empty
78 changes: 78 additions & 0 deletions test-d/jsonify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {expectAssignable, expectNotAssignable} from 'tsd';
import {Jsonify, JsonValue} from '..';

interface A {
a: number;
}

class B {
a!: number;
}

interface V {
a?: number;
}

interface X {
a: Date;
}

interface Y {
a?: Date;
}

interface Z {
a: number | undefined;
}

interface W {
a?: () => any;
}

declare const a: Jsonify<A>;
declare const b: Jsonify<B>;

declare const v: V; // Not assignable to JsonValue because it is defined as interface

declare const x: X; // Not assignable to JsonValue because it contains Date value
declare const y: Y; // Not assignable to JsonValue because it contains Date value

declare const z: Z; // Not assignable to JsonValue because undefined is not valid Json value
declare const w: W; // Not assignable to JsonValue because a function is not valid Json value

expectAssignable<JsonValue>(null);
expectAssignable<JsonValue>(false);
expectAssignable<JsonValue>(0);
expectAssignable<JsonValue>('');
expectAssignable<JsonValue>([]);
expectAssignable<JsonValue>({});
expectAssignable<JsonValue>([0]);
expectAssignable<JsonValue>({a: 0});
expectAssignable<JsonValue>(a);
expectAssignable<JsonValue>(b);
expectAssignable<JsonValue>({a: {b: true, c: {}}, d: [{}, 2, 'hi']});
expectAssignable<JsonValue>([{}, {a: 'hi'}, null, 3]);

expectNotAssignable<JsonValue>(new Date());
expectNotAssignable<JsonValue>([new Date()]);
expectNotAssignable<JsonValue>({a: new Date()});
expectNotAssignable<JsonValue>(v);
expectNotAssignable<JsonValue>(x);
expectNotAssignable<JsonValue>(y);
expectNotAssignable<JsonValue>(z);
expectNotAssignable<JsonValue>(w);
expectNotAssignable<JsonValue>(undefined);
expectNotAssignable<JsonValue>(5 as number | undefined);

interface Geometry {
type: 'Point' | 'Polygon';
coordinates: [number, number];
}

const point: Geometry = {
type: 'Point',
coordinates: [1, 1],
};

expectNotAssignable<JsonValue>(point);
expectAssignable<Jsonify<Geometry>>(point);

0 comments on commit 5349d27

Please sign in to comment.