Skip to content

Commit

Permalink
use opaque generic type to mark required fields
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeyraspopov committed Apr 19, 2024
1 parent 741920e commit 9c6c924
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 17 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dataclass",
"version": "3.0.0-beta.0",
"version": "3.0.0-beta.1",
"description": "Data classes for TypeScript & JavaScript",
"author": "Oleksii Raspopov",
"license": "ISC",
Expand Down
27 changes: 21 additions & 6 deletions typings/dataclass.d.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
type Optional<T> = { [P in keyof T as T[P] extends Exclude<T[P], undefined> ? never : P]: T[P] };
type Explicit<T> = { [P in keyof T as T[P] extends Exclude<T[P], undefined> ? P : never]: T[P] };
interface S {
/** If you see this, the property is coming from a dataclass instance and was required */
valueOf(): 2147483647;
}

/**
* Special generic type to mark data class field as required to be initialized
* at creation time. Can be used along with ! (exclamation mark operator) to
* define a field without a default value, but explicit type and make `create()`
* method require this field to be provided.
*/
export type Require<T> = T & S;

type Unwrap<T> = T extends Require<infer V> ? V : T;

type Optional<T> = { [P in keyof T as T[P] extends Require<T[P]> ? P : never]: Unwrap<T[P]> };
type Explicit<T> = { [P in keyof T as T[P] extends Require<T[P]> ? never : P]: T[P] };
type Init<T> = Required<Optional<Omit<T, keyof Data>>> & Partial<Explicit<Omit<T, keyof Data>>>;

/**
* Abstract class that allows defining custom data classes. Should be extended
* with a set of class fields that define the shape of desired model.
*
* ```ts
* import { Data } from "dataclass";
* import { Data, type Require } from "dataclass";
*
* class Project extends Data {
* // this property is required when creating an instance
* id?: string;
* id: Require<string>;
* // these properties have defaults but can be overwritten
* name: string = "Untitled";
* createdBy: string = "Anon";
Expand Down Expand Up @@ -46,7 +61,7 @@ export class Data {
static create<Type extends Data>(
this: { new (): Type },
...values: Optional<Type> extends Record<any, never> ? [Init<Type>?] : [Init<Type>]
): Required<Type>;
): Type;
/**
* Create new immutable instance based on existing one, with some properties changed.
*
Expand All @@ -63,7 +78,7 @@ export class Data {
* let updated = initial.copy({ age: 28 });
* ```
*/
copy(values?: Partial<Omit<this, keyof Data>>): Required<this>;
copy(values?: Partial<Omit<this, keyof Data>>): this;
/**
* Compare the instance to another instance of the same data class.
*
Expand Down
25 changes: 15 additions & 10 deletions typings/dataclass.js.flow
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
/* @flow */

declare type OptKeys<T> = $Values<{ [P in $Keys<T> ]: T[P] extends void ? P : empty }>;
declare type Optional<T> = Pick<{ [P in $Keys<T>]: T[P] }, OptKeys<T>>;
declare type Explicit<T> = Omit<{ [P in $Keys<T>]: T[P] }, OptKeys<T>>;
declare type Req<T> = { [P in $Keys<Optional<T>>]: $NonMaybeType<T[P]> };
declare type Opt<T> = { [P in $Keys<Explicit<T>>]: T[P]};
declare type Values<T> = { ...Req<T>, ...Partial<Opt<T>> };
declare type Required<T> = { ...Req<T>, ...Opt<T> };
/**
* Special generic type to mark data class field as required to be initialized
* at creation time. Can be used along with ! (exclamation mark operator) to
* define a field without a default value, but explicit type and make `create()`
* method require this field to be provided.
*/
declare export opaque type Require<T> : T;

declare type OptKeys<T> = $Values<{ [P in $Keys<T>]: T[P] extends Require<any> ? P : empty}>;
declare type Optional<T> = Pick<{ [P in $Keys<T>]: T[P] extends Require<infer V> ? V : T[P] }, OptKeys<T>>;
declare type Explicit<T> = Omit<{ [P in $Keys<T>]: T[P] extends Require<infer V> ? V : T[P] }, OptKeys<T>>;
declare type Values<T> = { ...Optional<T>, ...Partial<Explicit<T>> };
declare type Init<T> = Optional<T> extends Record<any, empty> ? Values<T> | void : Values<T>;
declare type Shape<T> = Partial<{ [P in $Keys<T>]: T[P] }>;
declare type Shape<T> = Partial<Values<T>>;

/**
* Abstract class that allows defining custom data classes. Should be extended
* with a set of class fields that define the shape of desired model.
*
* ```js
* // @flow
* import { Data } from "dataclass";
* import { Data, type Require } from "dataclass";
*
* class Project extends Data {
* // this property is required when creating an instance
* id: ?string;
* id: Require<string>;
* // these properties have defaults but can be overwritten
* name: string = "Untitled";
* createdBy: string = "Anon";
Expand Down

0 comments on commit 9c6c924

Please sign in to comment.