Skip to content

Commit

Permalink
Jsonify: Add jump to definition and any support (#519)
Browse files Browse the repository at this point in the history
Co-authored-by: Michaël De Boey <[email protected]>
Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
3 people authored Nov 28, 2022
1 parent 9bae03b commit 2071f47
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 48 deletions.
76 changes: 76 additions & 0 deletions source/internal.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {Primitive} from './primitive';
import type {Simplify} from './simplify';

/**
Returns a boolean for whether the two given types are equal.
Expand Down Expand Up @@ -108,3 +109,78 @@ export type IsUpperCase<T extends string> = T extends Uppercase<T> ? true : fals
Returns a boolean for whether the string is numeric.
*/
export type IsNumeric<T extends string> = T extends `${number}` ? true : false;

/**
Returns a boolean for whether the the type is `any`.
@link https://stackoverflow.com/a/49928360/1490091
*/
export type IsAny<T> = 0 extends 1 & T ? true : false;

/**
For an object T, if it has any properties that are a union with `undefined`, make those into optional properties instead.
@example
```
type User = {
firstName: string;
lastName: string | undefined;
};
type OptionalizedUser = UndefinedToOptional<User>;
//=> {
// firstName: string;
// lastName?: string;
// }
```
*/
export type UndefinedToOptional<T extends object> = Simplify<
{
// Property is not a union with `undefined`, keep it as-is.
[Key in keyof Pick<T, FilterDefinedKeys<T>>]: T[Key];
} & {
// Property _is_ a union with defined value. Set as optional (via `?`) and remove `undefined` from the union.
[Key in keyof Pick<T, FilterOptionalKeys<T>>]?: Exclude<T[Key], undefined>;
}
>;

// Returns `never` if the key or property is not jsonable without testing whether the property is required or optional otherwise return the key.
type BaseKeyFilter<Type, Key extends keyof Type> = Key extends symbol
? never
: Type[Key] extends symbol
? never
: [(...args: any[]) => any] extends [Type[Key]]
? never
: Key;

/**
Returns the required keys.
*/
type FilterDefinedKeys<T extends object> = Exclude<
{
[Key in keyof T]: IsAny<T[Key]> extends true
? Key
: undefined extends T[Key]
? never
: T[Key] extends undefined
? never
: BaseKeyFilter<T, Key>;
}[keyof T],
undefined
>;

/**
Returns the optional keys.
*/
type FilterOptionalKeys<T extends object> = Exclude<
{
[Key in keyof T]: IsAny<T[Key]> extends true
? never
: undefined extends T[Key]
? T[Key] extends undefined
? never
: BaseKeyFilter<T, Key>
: never;
}[keyof T],
undefined
>;
95 changes: 48 additions & 47 deletions source/jsonify.d.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
import type {JsonPrimitive, JsonValue} from './basic';
import type {EmptyObject} from './empty-object';
import type {Merge} from './merge';
import type {IsAny, UndefinedToOptional} from './internal';
import type {NegativeInfinity, PositiveInfinity} from './numeric';
import type {TypedArray} from './typed-array';

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

// Returns never if the key or property is not jsonable without testing whether the property is required or optional otherwise return the key.
type BaseKeyFilter<Type, Key extends keyof Type> = Key extends symbol
? never
: Type[Key] extends symbol
? never
: [(...args: any[]) => any] extends [Type[Key]]
? never
: Key;

// Returns never if the key or property is not jsonable or optional otherwise return the key.
type RequiredKeyFilter<Type, Key extends keyof Type> = undefined extends Type[Key]
? never
: BaseKeyFilter<Type, Key>;

// Returns never if the key or property is not jsonable or required otherwise return the key.
type OptionalKeyFilter<Type, Key extends keyof Type> = undefined extends Type[Key]
? Type[Key] extends undefined
? never
: BaseKeyFilter<Type, Key>
: never;
type JsonifyTuple<T extends [unknown, ...unknown[]]> = {
[Key in keyof T]: T[Key] extends NotJsonable ? null : Jsonify<T[Key]>;
};

type FilterJsonableKeys<T extends object> = {
[Key in keyof T]: T[Key] extends NotJsonable ? never : Key;
}[keyof T];

/**
JSON serialize objects (not including arrays) and classes.
*/
type JsonifyObject<T extends object> = {
[Key in keyof Pick<T, FilterJsonableKeys<T>>]: Jsonify<T[Key]>;
};

/**
Transform a type to one that is assignable to the `JsonValue` type.
Expand Down Expand Up @@ -85,29 +79,36 @@ const timeJson = JSON.parse(JSON.stringify(time)) as Jsonify<typeof time>;
@category JSON
*/
export 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 | bigint>] extends [never]
? T extends PositiveInfinity | NegativeInfinity ? null
: T extends JsonPrimitive ? T // Primitive is acceptable
: T extends object
// Any object with toJSON is special case
? T extends {toJSON(): infer J} ? (() => J) extends (() => JsonValue) // Is J assignable to JsonValue?
? J // Then T is Jsonable and its Jsonable value is J
: Jsonify<J> // Maybe if we look a level deeper we'll find a JsonValue
// Instanced primitives are objects
: T extends Number ? number
: T extends String ? string
: T extends Boolean ? boolean
: T extends Map<any, any> | Set<any> ? EmptyObject
: T extends TypedArray ? Record<string, number>
: T extends any[]
? {[I in keyof T]: T[I] extends NotJsonable ? null : Jsonify<T[I]>}
: Merge<
{[Key in keyof T as RequiredKeyFilter<T, Key>]: Jsonify<T[Key]>},
{[Key in keyof T as OptionalKeyFilter<T, Key>]?: Jsonify<Exclude<T[Key], undefined>>}
> // Recursive call for its children
: never // Otherwise any other non-object is removed
: never; // Otherwise non-JSONable type union was found not empty
export type Jsonify<T> = IsAny<T> extends true
? any
: T extends PositiveInfinity | NegativeInfinity
? null
: T extends JsonPrimitive
? T
: // Instanced primitives are objects
T extends Number
? number
: T extends String
? string
: T extends Boolean
? boolean
: T extends Map<any, any> | Set<any>
? EmptyObject
: T extends TypedArray
? Record<string, number>
: T extends NotJsonable
? never // Non-JSONable type union was found not empty
: // Any object with toJSON is special case
T extends {toJSON(): infer J}
? (() => J) extends () => JsonValue // Is J assignable to JsonValue?
? J // Then T is Jsonable and its Jsonable value is J
: Jsonify<J> // Maybe if we look a level deeper we'll find a JsonValue
: T extends []
? []
: T extends [unknown, ...unknown[]]
? JsonifyTuple<T>
: T extends ReadonlyArray<infer U>
? Array<U extends NotJsonable ? null : Jsonify<U>>
: T extends object
? JsonifyObject<UndefinedToOptional<T>> // JsonifyObject recursive call for its children
: never; // Otherwise any other non-object is removed
3 changes: 2 additions & 1 deletion source/set-return-type.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
type IsAny<T> = 0 extends (1 & T) ? true : false; // https://stackoverflow.com/a/49928360/3406963
import type {IsAny} from './internal';

type IsNever<T> = [T] extends [never] ? true : false;
type IsUnknown<T> = IsNever<T> extends false ? T extends unknown ? unknown extends T ? IsAny<T> extends false ? true : false : false : false : false;

Expand Down
10 changes: 10 additions & 0 deletions test-d/jsonify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,13 @@ type ExpectedAppDataJson = {
declare const response: Jsonify<AppData>;

expectType<ExpectedAppDataJson>(response);

expectType<any>({} as Jsonify<any>);

declare const objectWithAnyProperty: Jsonify<{
a: any;
}>;
expectType<{a: any}>(objectWithAnyProperty);

declare const objectWithAnyProperties: Jsonify<Record<string, any>>;
expectType<Record<string, any>>(objectWithAnyProperties);

0 comments on commit 2071f47

Please sign in to comment.