rsuite / schema-typed

Schema for data modeling & validation
MIT License
198 stars 28 forks source link

Typescript types declaration. #19

Open majo44 opened 5 years ago

majo44 commented 5 years ago

As this becomes as an standard, and allows wider adoption, it is worth to add to library the typescript types declaration.

simonguo commented 5 years ago

Typescript types declaration in rsuite https://github.com/rsuite/rsuite/blob/master/src/Schema/index.d.ts

Usage https://rsuitejs.com/en/components/schema

majo44 commented 5 years ago

Hi, thanks, good to know that there is declaration:

I very like that lib, have simple reasonable api, supports es6, and do not have any dependencies, I checked 20 different similar libs, and I very like to use that one in my project, and I do not using RSuite as I do not using the React there at all, so:

  1. why is not add this declaration to the package, to simplify the consumption
  2. maybe we can create a bit more expressive declaration which can be used together with static generic types, that is example how such declaration can be checked during compilation:
interface A {a: string, b?: number}

// Example of schema with provided type
const typedSchema = new Schema<A>({
    a: NumberType(), // Error Type 'INumberType<Partial<A>>' is not assignable to type 'IType<string, Partial<A>>'... Type 'number' is not assignable to type 'string'.
    // for autocast of data in addRule we can also add type here
    b: NumberType<A>().isRequired('Required').addRule((value, data) => {
        let v: string = value; // Error: Type 'number' is not assignable to type 'string'
        let d: {x: any} = data; // Error: Property 'x' is missing in type 'A' but required in type '{ x: any; }'.
    }),
    c: BooleanType(), // Error: Argument of type '{ a: INumberType<any>; b: INumberType<A>; c: IBooleanType<any>; }' is not assignable to parameter of type 'ISchema<Partial<A>>'.
});

typedSchema.check({ c: 12}); //Error: Argument of type '{ c: number; }' is not assignable to parameter of type 'A'
typedSchema.checkForField('x', '12'); //Error: Argument of type '"x"' is not assignable to parameter of type '"a" | "b" | "c"'.

// Example of schema without provided type
// The validation object type will be inferred
const noTypedSchema = new Schema({
    a: NumberType(),
    b: NumberType().isRequired('Required').addRule((value, data) => {
        let v: string = value; // Error: Type 'number' is not assignable to type 'string'
        let d: {x: any} = data;
    }),
    c: BooleanType(),
});

noTypedSchema.check({ c: 12}); //Error: Type 'number' is not assignable to type 'boolean'.
noTypedSchema.checkForField('x', '12'); //Error: Argument of type '"x"' is not assignable to parameter of type '"a" | "b" | "c"'.

// Example of schema with any type provided
const anySchema = new Schema<any>({
    a: NumberType(),
    b: NumberType().isRequired('Required').addRule((value, data) => {
        let v: string = value; // Error: Type 'number' is not assignable to type 'string'
        let d: {x: any} = data;
    }),
    c: BooleanType(),
});

anySchema.check({ c: 12});
anySchema.checkForField('x', '12');

You can plat with that here

I created prototype of such declaration which allows such static checks:

export type RuleFn<V, D> = (value: V, data: D) =>
    FieldSchemaCheckResult | boolean  | void | undefined | Promise<boolean> | Promise<FieldSchemaCheckResult> | Promise<void>;

export interface IType<P, T> {
  addRule(onValid: RuleFn<P, T>, errorMessage?: string, priority?: boolean): this;
}

export interface IStringType<T = any> extends IType<string, T> {
  isRequired(errorMessage?: string, trim?: boolean): this;
  isEmail(errorMessage?: string): this;
  isURL(errorMessage?: string): this;
  isOneOf(items: Array<string>, errorMessage?: string): this;
  containsLetter(errorMessage?: string): this;
  containsUppercaseLetter(errorMessage?: string): this;
  containsLowercaseLetter(errorMessage?: string): this;
  containsLetterOnly(errorMessage?: string): this;
  containsNumber(errorMessage?: string): this;
  pattern(regExp: RegExp, errorMessage?: string): this;
  rangeLength(minLength: number, maxLength: number, errorMessage?: string): this;
  minLength(minLength: number, errorMessage?: string): this;
  maxLength(maxLength: number, errorMessage?: string): this;
}

export interface INumberType<T = any> extends IType<number, T>  {
  isRequired(errorMessage?: string): this;
  isInteger(errorMessage?: string): this;
  isOneOf(items: Array<number>, errorMessage?: string): this;
  pattern(regExp: RegExp, errorMessage?: string): this;
  range(minLength: number, maxLength: number, errorMessage?: string): this;
  min(min: number, errorMessage?: string): this;
  max(max: number, errorMessage?: string): this;
}

export interface IArrayType<I = any, T = any> extends IType<Array<I>, T>  {
  isRequired(errorMessage?: string): this;
  rangeLength(minLength: number, maxLength: number, errorMessage?: string): this;
  minLength(minLength: number, errorMessage?: string): this;
  maxLength(maxLength: number, errorMessage?: string): this;
  unrepeatable(errorMessage?: string): this;
  of(type: IType<I, T>, errorMessage?: string): this;
}

export interface IDateType<T = any> extends IType<Date, T> {
  isRequired(errorMessage?: string): this;
  range(min: Date, max: Date, errorMessage?: string): this;
  min(min: Date, errorMessage?: string): this;
  max(max: Date, errorMessage?: string): this;
}

export interface IObjectType<O = any, T = any> extends IType<O, T> {
  isRequired(errorMessage?: string): this;
  shape(shape: ISchema<O>): this;
}

export interface IBooleanType<T = any> extends IType<boolean, T>  {
  isRequired(errorMessage?: string): this;
}

type IType<X, T> =
  T extends any ? any :
    X extends string ? IStringType<T>:
        X extends number ? INumberType<T>:
            X extends boolean ? IBooleanType<T>:
                X extends Date ? IDateType<T>:
                    X extends Array<infer I> ? IArrayType<I, T>:
                        X extends any ?
                            IStringType<T> | INumberType<T> | IBooleanType<T> | IArrayType<any, T> | IDateType<T> | IObjectType<X, T> :
                            IObjectType<X, T>;

export type ISchema<T> = {
  [P in keyof T]: IType<T[P], T>;
}

export type FieldSchemaCheckResult = {
  hasError: boolean,
  errorMessage: string
}

export type SchemaCheckResult<T> = {
  [P in keyof T]: FieldSchemaCheckResult
};

export interface ISchemaModel<T = any> {
  schema: ISchema<T>;
  check(data: T): SchemaCheckResult<T>;
  checkAsync(data: T): Promise<SchemaCheckResult<T>>;
  checkForField<K extends keyof T>(fieldName: K, fieldValue: T[K], data?: T): FieldSchemaCheckResult;
  checkForFieldAsync<K extends keyof T>(fieldName: K, fieldValue: T[K], data?: T): Promise<FieldSchemaCheckResult>;
  getFieldType<K extends keyof T>(fieldName: K): IType<T[K], T>;
  getKeys(): Array<PropertyKey>;
}

export interface ISchemaModelFactory {
  <T>(schema: ISchema<Partial<T>>): ISchemaModel<T>;
  combine<T>(...schemas: Array<ISchemaModel<Partial<T>>>): ISchemaModel<T>;
}

export declare const SchemaModel: ISchemaModelFactory;
export declare function StringType<T = any>(errorMessage?: string): IStringType<T>;
export declare function NumberType<T = any>(errorMessage?: string): INumberType<T>;
export declare function BooleanType<T = any>(errorMessage?: string): IBooleanType<T>;
export declare function ArrayType<I = any, T = any>(errorMessage?: string): IArrayType<I, T>;
export declare function DateType<T = any>(errorMessage?: string): IDateType<T>;
export declare function ObjectType<O = any, T = any>(errorMessage?: string): IObjectType<O, T>;
export declare class Schema<T = any> {
  constructor(schema: ISchema<Partial<T>>);
  check(data: T): SchemaCheckResult<T>;
  checkAsync(data: T): Promise<SchemaCheckResult<T>>;
  checkForField<K extends keyof T>(fieldName: K, fieldValue: T[K], data?: T): FieldSchemaCheckResult;
  checkForFieldAsync<K extends keyof T>(fieldName: K, fieldValue: T[K], data?: T): Promise<FieldSchemaCheckResult>;
  schema: ISchema<T>;
  getFieldType<K extends keyof T>(fieldName: K): IType<T[K], T>;
  getKeys(): Array<PropertyKey>;
}
simonguo commented 5 years ago

schema-typed should indeed have a separate typescript type definition. If you like, I hope you can submit a PR.