|
1 | 1 | import type { |
2 | 2 | FormError, |
3 | 3 | FormValue, |
| 4 | + FieldName, |
4 | 5 | JsonPrimitive, |
5 | 6 | Serialize, |
6 | 7 | SerializedValue, |
7 | 8 | Submission, |
8 | 9 | SubmissionResult, |
| 10 | + UnknownObject, |
9 | 11 | } from './types'; |
10 | 12 | import { isGlobalInstance, isSubmitter } from './dom'; |
11 | | -import { deepEqual, isPlainObject, stripFiles } from './util'; |
| 13 | +import { deepEqual, getTypeName, isPlainObject, stripFiles } from './util'; |
12 | 14 | import type { StandardSchemaIssue } from './standard-schema'; |
13 | 15 | import { formatIssues } from './standard-schema'; |
14 | 16 |
|
@@ -872,3 +874,172 @@ export function serialize(value: unknown): SerializedValue | null | undefined { |
872 | 874 |
|
873 | 875 | return serializePrimitive(value); |
874 | 876 | } |
| 877 | + |
| 878 | +/** |
| 879 | + * Retrieve a field value from FormData with optional type guards. |
| 880 | + * |
| 881 | + * @example |
| 882 | + * // Basic field access: return `unknown` |
| 883 | + * const email = getFieldValue(formData, 'email'); |
| 884 | + * // String type: returns `string` |
| 885 | + * const name = getFieldValue(formData, 'name', { type: 'string' }); |
| 886 | + * // File type: returns `File` |
| 887 | + * const avatar = getFieldValue(formData, 'avatar', { type: 'file' }); |
| 888 | + * // Object type: returns { city: unknown, ... } |
| 889 | + * const address = getFieldValue<Address>(formData, 'address', { type: 'object' }); |
| 890 | + * // Array: returns `unknown[]` |
| 891 | + * const tags = getFieldValue(formData, 'tags', { array: true }); |
| 892 | + * // Array with object type: returns `Array<{ name: unknown, ... }>` |
| 893 | + * const items = getFieldValue<Item[]>(formData, 'items', { type: 'object', array: true }); |
| 894 | + * // Optional string type: returns `string | undefined` |
| 895 | + * const bio = getFieldValue(formData, 'bio', { type: 'string', optional: true }); |
| 896 | + */ |
| 897 | +export function getFieldValue< |
| 898 | + FieldShape extends Array<Record<string, unknown>>, |
| 899 | +>( |
| 900 | + formData: FormData | URLSearchParams, |
| 901 | + name: FieldName<FieldShape>, |
| 902 | + options: { type: 'object'; array: true; optional: true }, |
| 903 | +): FieldShape extends Array<infer Item extends Record<string, unknown>> |
| 904 | + ? Array<UnknownObject<Item>> | undefined |
| 905 | + : never; |
| 906 | +export function getFieldValue< |
| 907 | + FieldShape extends Array<Record<string, unknown>>, |
| 908 | +>( |
| 909 | + formData: FormData | URLSearchParams, |
| 910 | + name: FieldName<FieldShape>, |
| 911 | + options: { type: 'object'; array: true }, |
| 912 | +): FieldShape extends Array<infer Item extends Record<string, unknown>> |
| 913 | + ? Array<UnknownObject<Item>> |
| 914 | + : never; |
| 915 | +export function getFieldValue< |
| 916 | + FieldShape extends Record<string, unknown> = Record<string, unknown>, |
| 917 | +>( |
| 918 | + formData: FormData | URLSearchParams, |
| 919 | + name: FieldName<FieldShape>, |
| 920 | + options: { type: 'object'; optional: true }, |
| 921 | +): UnknownObject<FieldShape> | undefined; |
| 922 | +export function getFieldValue< |
| 923 | + FieldShape extends Record<string, unknown> = Record<string, unknown>, |
| 924 | +>( |
| 925 | + formData: FormData | URLSearchParams, |
| 926 | + name: FieldName<FieldShape>, |
| 927 | + options: { type: 'object' }, |
| 928 | +): UnknownObject<FieldShape>; |
| 929 | +export function getFieldValue<FieldShape>( |
| 930 | + formData: FormData | URLSearchParams, |
| 931 | + name: FieldName<FieldShape>, |
| 932 | + options: { type: 'string'; array: true; optional: true }, |
| 933 | +): string[] | undefined; |
| 934 | +export function getFieldValue<FieldShape>( |
| 935 | + formData: FormData | URLSearchParams, |
| 936 | + name: FieldName<FieldShape>, |
| 937 | + options: { type: 'string'; array: true }, |
| 938 | +): string[]; |
| 939 | +export function getFieldValue<FieldShape>( |
| 940 | + formData: FormData | URLSearchParams, |
| 941 | + name: FieldName<FieldShape>, |
| 942 | + options: { type: 'string'; optional: true }, |
| 943 | +): string | undefined; |
| 944 | +export function getFieldValue<FieldShape>( |
| 945 | + formData: FormData | URLSearchParams, |
| 946 | + name: FieldName<FieldShape>, |
| 947 | + options: { type: 'string' }, |
| 948 | +): string; |
| 949 | +export function getFieldValue<FieldShape>( |
| 950 | + formData: FormData | URLSearchParams, |
| 951 | + name: FieldName<FieldShape>, |
| 952 | + options: { type: 'file'; array: true; optional: true }, |
| 953 | +): File[] | undefined; |
| 954 | +export function getFieldValue<FieldShape>( |
| 955 | + formData: FormData | URLSearchParams, |
| 956 | + name: FieldName<FieldShape>, |
| 957 | + options: { type: 'file'; array: true }, |
| 958 | +): File[]; |
| 959 | +export function getFieldValue<FieldShape>( |
| 960 | + formData: FormData | URLSearchParams, |
| 961 | + name: FieldName<FieldShape>, |
| 962 | + options: { type: 'file'; optional: true }, |
| 963 | +): File | undefined; |
| 964 | +export function getFieldValue<FieldShape>( |
| 965 | + formData: FormData | URLSearchParams, |
| 966 | + name: FieldName<FieldShape>, |
| 967 | + options: { type: 'file' }, |
| 968 | +): File; |
| 969 | +export function getFieldValue<FieldShape>( |
| 970 | + formData: FormData | URLSearchParams, |
| 971 | + name: FieldName<FieldShape>, |
| 972 | + options: { array: true; optional: true }, |
| 973 | +): Array<unknown> | undefined; |
| 974 | +export function getFieldValue<FieldShape>( |
| 975 | + formData: FormData | URLSearchParams, |
| 976 | + name: FieldName<FieldShape>, |
| 977 | + options: { array: true }, |
| 978 | +): Array<unknown>; |
| 979 | +export function getFieldValue<FieldShape>( |
| 980 | + formData: FormData | URLSearchParams, |
| 981 | + name: FieldName<FieldShape>, |
| 982 | + options?: { optional?: boolean }, |
| 983 | +): unknown; |
| 984 | +export function getFieldValue<FieldShape>( |
| 985 | + formData: FormData | URLSearchParams, |
| 986 | + name: FieldName<FieldShape>, |
| 987 | + options?: { |
| 988 | + type?: 'object' | 'string' | 'file'; |
| 989 | + array?: boolean; |
| 990 | + optional?: boolean; |
| 991 | + }, |
| 992 | +): unknown { |
| 993 | + const { type, array, optional } = options ?? {}; |
| 994 | + |
| 995 | + let value: unknown; |
| 996 | + |
| 997 | + // Check if formData has a direct entry |
| 998 | + if (formData.has(name)) { |
| 999 | + // Get value based on array option |
| 1000 | + value = array ? formData.getAll(name) : formData.get(name); |
| 1001 | + } else { |
| 1002 | + // Parse formData and use getValueAtPath |
| 1003 | + const submission = parseSubmission(formData, { |
| 1004 | + stripEmptyValues: false, |
| 1005 | + }); |
| 1006 | + value = getValueAtPath(submission.payload, name); |
| 1007 | + } |
| 1008 | + |
| 1009 | + // If optional and value is undefined, skip validation and return early |
| 1010 | + if (optional && value === undefined) { |
| 1011 | + return; |
| 1012 | + } |
| 1013 | + |
| 1014 | + // Type guards - validate the value matches the expected type |
| 1015 | + if (array && !Array.isArray(value)) { |
| 1016 | + throw new Error( |
| 1017 | + `Expected field "${name}" to be an array, but got ${getTypeName(value)}`, |
| 1018 | + ); |
| 1019 | + } |
| 1020 | + |
| 1021 | + if (type) { |
| 1022 | + const items = array ? (value as unknown[]) : [value]; |
| 1023 | + const predicate = { |
| 1024 | + string: (v: unknown) => typeof v === 'string', |
| 1025 | + file: (v: unknown) => v instanceof File, |
| 1026 | + object: isPlainObject, |
| 1027 | + }[type]; |
| 1028 | + const typeName = { |
| 1029 | + string: 'a string', |
| 1030 | + file: 'a File', |
| 1031 | + object: 'an object', |
| 1032 | + }[type]; |
| 1033 | + |
| 1034 | + for (let i = 0; i < items.length; i++) { |
| 1035 | + if (!predicate(items[i])) { |
| 1036 | + const field = array ? `${name}[${i}]` : name; |
| 1037 | + throw new Error( |
| 1038 | + `Expected field "${field}" to be ${typeName}, but got ${getTypeName(items[i])}`, |
| 1039 | + ); |
| 1040 | + } |
| 1041 | + } |
| 1042 | + } |
| 1043 | + |
| 1044 | + return value; |
| 1045 | +} |
0 commit comments