Skip to content

feat: Array support explicit type #83

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Schema for data modeling & validation
- [`minLength(minLength: number, errorMessage?: string)`](#minlengthminlength-number-errormessage-string-1)
- [`maxLength(maxLength: number, errorMessage?: string)`](#maxlengthmaxlength-number-errormessage-string-1)
- [`unrepeatable(errorMessage?: string)`](#unrepeatableerrormessage-string)
- [`of(type: object)`](#oftype-object)
- [`of()`](#of)
- [DateType(errorMessage?: string)](#datetypeerrormessage-string)
- [`range(min: Date, max: Date, errorMessage?: string)`](#rangemin-date-max-date-errormessage-string)
- [`min(min: Date, errorMessage?: string)`](#minmin-date-errormessage-string)
Expand Down Expand Up @@ -726,10 +726,24 @@ ArrayType().maxLength(3, "Can't exceed three");
ArrayType().unrepeatable('Duplicate options cannot appear');
```

#### `of(type: object)`
#### `of()`

```js
// for every element of array
ArrayType().of(StringType('The tag should be a string').isRequired());
// for every element of array
ArrayType().of(
ObjectType().shape({
name: StringType().isEmail()
})
);
// just specify the first and the second element
ArrayType().of(
StringType().isEmail(),
ObjectType().shape({
name: StringType().isEmail()
})
);
```

### DateType(errorMessage?: string)
Expand Down
90 changes: 56 additions & 34 deletions src/ArrayType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class ArrayType<DataType = any, E = ErrorMessageType> extends MixedType<
E,
ArrayTypeLocale
> {
[arrayTypeSchemaSpec]: MixedType<any, DataType, E>;
[arrayTypeSchemaSpec]: MixedType<any, DataType, E> | MixedType<any, DataType, E>[];
private isArrayTypeNested = false;

constructor(errorMessage?: E | string) {
Expand Down Expand Up @@ -75,44 +75,66 @@ export class ArrayType<DataType = any, E = ErrorMessageType> extends MixedType<
return this;
}

of(type: MixedType<any, DataType, E>) {
this[arrayTypeSchemaSpec] = type;

// Mark inner ArrayType as nested when dealing with nested arrays
if (type instanceof ArrayType) {
type.isArrayTypeNested = true;
}
of(...types: MixedType<any, DataType, E>[]) {
if (types.length === 1) {
const type = types[0];
this[arrayTypeSchemaSpec] = type;

super.pushRule({
onValid: (items, data, fieldName) => {
// For non-array values in nested arrays, pass directly to inner type validation
if (!Array.isArray(items) && this.isArrayTypeNested) {
return type.check(items, data, fieldName);
}
// Mark inner ArrayType as nested when dealing with nested arrays
if (type instanceof ArrayType) {
type.isArrayTypeNested = true;
}

// For non-array values in non-nested arrays, return array type error
if (!Array.isArray(items)) {
return {
hasError: true,
errorMessage: this.locale.type
};
}
super.pushRule({
onValid: (items, data, fieldName) => {
// For non-array values in nested arrays, pass directly to inner type validation
if (!Array.isArray(items) && this.isArrayTypeNested) {
return type.check(items, data, fieldName);
}

const checkResults = items.map((value, index) => {
const name = Array.isArray(fieldName)
? [...fieldName, `[${index}]`]
: [fieldName, `[${index}]`];
// For non-array values in non-nested arrays, return array type error
if (!Array.isArray(items)) {
return {
hasError: true,
errorMessage: this.locale.type
};
}

return type.check(value, data, name as string[]);
});
const hasError = !!checkResults.find(item => item?.hasError);
const checkResults = items.map((value, index) => {
const name = Array.isArray(fieldName)
? [...fieldName, `[${index}]`]
: [fieldName, `[${index}]`];

return {
hasError,
array: checkResults
} as CheckResult<string | E>;
}
});
return type.check(value, data, name as string[]);
});
const hasError = !!checkResults.find(item => item?.hasError);

return {
hasError,
array: checkResults
} as CheckResult<string | E>;
}
});
} else {
this[arrayTypeSchemaSpec] = types;
super.pushRule({
onValid: (items, data, fieldName) => {
const checkResults = items.map((value, index) => {
const name = Array.isArray(fieldName)
? [...fieldName, `[${index}]`]
: [fieldName, `[${index}]`];

return types[index].check(value, data, name as string[]);
});
const hasError = !!checkResults.find(item => item?.hasError);

return {
hasError,
array: checkResults
} as CheckResult<string | E>;
}
});
}

return this;
}
Expand Down
60 changes: 38 additions & 22 deletions src/MixedType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,34 +32,50 @@ export const arrayTypeSchemaSpec = 'arrayTypeSchemaSpec';
* Get the field type from the schema object
*/
export function getFieldType(schemaSpec: any, fieldName: string, nestedObject?: boolean) {
if (nestedObject) {
const namePath = fieldName.split('.');
const currentField = namePath[0];
const arrayMatch = currentField.match(/(\w+)\[(\d+)\]/);
if (schemaSpec) {
if (nestedObject) {
const namePath = fieldName.split('.');
const currentField = namePath[0];
const arrayMatch = currentField.match(/(\w+)\[(\d+)\]/);
if (arrayMatch) {
const [, arrayField, arrayIndex] = arrayMatch;
const type = schemaSpec[arrayField];
if (type?.[arrayTypeSchemaSpec]) {
const arrayType = type[arrayTypeSchemaSpec];

if (arrayMatch) {
const [, arrayField] = arrayMatch;
const type = schemaSpec[arrayField];
if (namePath.length > 1) {
if (arrayType[schemaSpecKey]) {
return getFieldType(arrayType[schemaSpecKey], namePath.slice(1).join('.'), true);
}
if (Array.isArray(arrayType) && arrayType[parseInt(arrayIndex)][schemaSpecKey]) {
return getFieldType(
arrayType[parseInt(arrayIndex)][schemaSpecKey],
namePath.slice(1).join('.'),
true
);
}
}
if (Array.isArray(arrayType)) {
return arrayType[parseInt(arrayIndex)];
}
// Otherwise return the array element type directly
return arrayType;
}
return type;
} else {
const type = schemaSpec[currentField];

if (type?.[arrayTypeSchemaSpec]) {
// If there are remaining paths and the type is ObjectType (has schemaSpecKey)
if (namePath.length > 1 && type[arrayTypeSchemaSpec][schemaSpecKey]) {
return getFieldType(
type[arrayTypeSchemaSpec][schemaSpecKey],
namePath.slice(1).join('.'),
true
);
if (namePath.length === 1) {
return type;
}

if (namePath.length > 1 && type[schemaSpecKey]) {
return getFieldType(type[schemaSpecKey], namePath.slice(1).join('.'), true);
}
// Otherwise return the array element type directly
return type[arrayTypeSchemaSpec];
}
return type;
}

const joinedPath = namePath.join(`.${schemaSpecKey}.`);
return get(schemaSpec, joinedPath);
return schemaSpec?.[fieldName];
}
return schemaSpec?.[fieldName];
}

/**
Expand Down
Loading