提问人:kohloth 提问时间:11/15/2023 最后编辑:kohloth 更新时间:11/15/2023 访问量:45
如何确保 Typescript 类型递归函数正确?
How to ensure Typescript types recursive functions correctly?
问:
我面临着一个非常棘手的打字稿问题。
我正在编写一个小型用户输入验证器库,用于验证可以无限嵌套的用户输入位。一段用户输入可以是布尔值、数字、字符串(基元)或数组或对象之一。
下面的测试显示了它是如何工作的(下面的链接将带您进入整个库的演示 - 除了此类型问题外,所有工作都正常工作)。
it('validates an invalid array of objs of prims correctly', () => {
const value = [
{ name: 'adam' },
{ name: 'jade' },
{ name: 's' },
];
const descriptor: ValidatorArrayFieldDescriptor = {
required: true,
type: 'array',
children: {
type: 'object',
children: {
name: {
type: 'string',
rules: [
{ type: 'min', params: { size: 2 } }
],
},
},
},
};
const result = validator.checkValue({
value,
descriptor,
});
expect(result.messages.length).toBe(0);
expect(result.children[0].children.name.messages.length).toBe(0);
expect(result.children[0].children.name.isValid).toBe(true);
expect(result.children[1].children.name.messages.length).toBe(0);
expect(result.children[1].children.name.isValid).toBe(true);
expect(result.children[2].children.name.messages.length).toBe(1);
expect(result.children[2].children.name.isValid).toBe(false);
expect(result.isValid).toBe(false);
expect(result.selfIsValid).toBe(true);
expect(result.childrenAreValid).toBe(false);
});
全面实施在这里
https://codesandbox.io/s/snowy-forest-2wcxjz?file=/src/validator.ts
此处还附上了最小示例
import { startCase } from "lodash";
// ############################################################################
// Utils
// ############################################################################
function formatPropKey(rawName: string): string {
return utilFns.ucFirst(startCase(rawName).toLowerCase());
}
function isObject(obj: any) {
if (typeof obj === "boolean") return false;
if (typeof obj === "number") return false;
if (typeof obj === "string") return false;
if (typeof obj === "undefined") return false;
if (Array.isArray(obj)) return false;
let out = false;
try {
Object.keys(obj);
} catch (err) {
return false;
}
return true;
}
function ucFirst(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function ensureArray<T = any>(arr: any): T[] {
if (Array.isArray(arr)) return arr as T[];
if (!arr) return [] as T[];
return [arr] as T[];
}
const utilFns = {
isObject,
ucFirst,
ensureArray
};
// ############################################################################
// Types
// ############################################################################
export type ValidatorRuleType =
| "min"
| "max"
| "pattern"
// ====
export interface ValidatorRuleMinParams {
size: number;
}
// ====
export interface ValidatorRuleMaxParams {
size: number;
}
// ====
export interface ValidatorRulePatternParams {
pattern: RegExp;
}
// ===
export interface ValidatorTagValidityInfo {
subMessage: string;
}
// ===
export interface ValidatorTagsValidityInfo {
subMessage: string;
plural: boolean;
}
// ===
export interface ValidatorBlackperiodsValidityInfo {
subMessage: string;
plural: boolean;
}
// ====
export type AnyValidatorRuleParams =
| ValidatorRuleMinParams
| ValidatorRuleMaxParams
| ValidatorRulePatternParams
| ValidatorRuleOneOfParams;
export type AnyValidityInfo =
| ValidatorTagValidityInfo
| ValidatorTagsValidityInfo
| ValidatorBlackperiodsValidityInfo;
// ============================================================================
export interface BuiltInValidatorParams<
T = AnyValidatorRuleParams | undefined
> {
value: any;
dataType: ValidatorFieldDataType;
ruleParams: T;
}
export interface BuiltInValidatorMessageGetterParams<
RuleParams = AnyValidatorRuleParams | undefined,
ValidityInfo = AnyValidityInfo | undefined
> {
propKey: string;
dataType: ValidatorFieldDataType;
ruleParams: RuleParams;
validityInfo: ValidityInfo;
}
// ============================================================================
export type ValidatorFieldDataType =
| "string"
| "boolean"
| "number"
| "array"
| "object";
// ============================================================================
export interface ValidatorFieldRule {
type: ValidatorRuleType;
params: any;
}
export interface ValidatorBooleanFieldDescriptor {
type: "boolean";
required?: boolean;
rules?: ValidatorFieldRule[];
}
export interface ValidatorNumberFieldDescriptor {
type: "number";
required?: boolean;
rules?: ValidatorFieldRule[];
}
export interface ValidatorStringFieldDescriptor {
type: "string";
required?: boolean;
rules?: ValidatorFieldRule[];
}
export interface ValidatorArrayFieldDescriptor {
type: "array";
required?: boolean;
rules?: ValidatorFieldRule[];
children: AnyValidatorFieldDescriptor;
}
export interface ValidatorObjectFieldDescriptor {
type: "object";
required?: boolean;
rules?: ValidatorFieldRule[];
children: { [key: string]: AnyValidatorFieldDescriptor };
}
export type AnyValidatorFieldDescriptor =
| ValidatorBooleanFieldDescriptor
| ValidatorNumberFieldDescriptor
| ValidatorStringFieldDescriptor
| ValidatorArrayFieldDescriptor
| ValidatorObjectFieldDescriptor;
export type PickValidatorFieldDescriptor<
T extends ValidatorFieldDataType
> = T extends "boolean"
? ValidatorBooleanFieldDescriptor
: T extends "number"
? ValidatorNumberFieldDescriptor
: T extends "string"
? ValidatorStringFieldDescriptor
: T extends "array"
? ValidatorArrayFieldDescriptor
: T extends "object"
? ValidatorObjectFieldDescriptor
: never;
// ============================================================================
export interface ValidatorFieldPrimativeResult {
isValid: boolean;
messages: string[];
}
export interface ValidatorFieldArrayResult {
isValid: boolean;
messages: string[];
selfIsValid: boolean;
childrenAreValid: boolean;
children: ValidatorFieldPrimativeResult[];
}
export interface ValidatorFieldObjectResult {
isValid: boolean;
messages: string[];
selfIsValid: boolean;
childrenAreValid: boolean;
children: {
[key: string]: ValidatorFieldPrimativeResult;
};
}
export type PickValidatorFieldResult<
T extends ValidatorFieldDataType
> = T extends "boolean"
? ValidatorFieldPrimativeResult
: T extends "number"
? ValidatorFieldPrimativeResult
: T extends "string"
? ValidatorFieldPrimativeResult
: T extends "array"
? ValidatorFieldArrayResult
: T extends "object"
? ValidatorFieldObjectResult
: never;
export type AnyValidatorFieldResult =
| ValidatorFieldPrimativeResult
| ValidatorFieldArrayResult
| ValidatorFieldObjectResult;
// ############################################################################
// Built in system checks
// ############################################################################
export const builtInSystemChecks = {
required: (propKey: string) => {
return `${formatPropKey(propKey)} must be supplied.`;
},
incorrectFormat: (propKey: string, expected: string, got: string) => {
return `${formatPropKey(
propKey
)} should be a ${expected}, but as a ${got}.`;
}
};
// ############################################################################
// Built-in validators
// ############################################################################
export const builtInValidators: {
[key: string]: {
message: (params: any) => string;
fn: (
params: any
) => {
valid: boolean;
validityInfo?: any;
};
};
} = {
min: {
message: (
params: BuiltInValidatorMessageGetterParams<ValidatorRuleMinParams>
) => {
const { propKey, dataType, ruleParams } = params;
const { size } = ruleParams;
if (dataType === "string") {
return `${formatPropKey(
propKey
)} must have no fewer than ${size} characters`;
} else if (dataType === "number") {
return `${formatPropKey(propKey)} must be no smaller than ${size}`;
} else if (dataType === "array" || dataType === "object") {
return `${formatPropKey(
propKey
)} must have no fewer than ${size} items`;
} else {
throw new Error(
`The rule "min" cannot be used on a datatype of "${dataType}".`
);
}
},
fn: (params: BuiltInValidatorParams<ValidatorRuleMinParams>) => {
const { value, dataType, ruleParams } = params;
const { size } = ruleParams;
const sizeValue = (() => {
if (dataType === "string") {
return value.length;
} else if (dataType === "number") {
return value;
} else if (dataType === "array") {
return value.length;
} else if (dataType === "object") {
return Object.keys(value).length;
} else {
throw new Error(
`The rule "min" cannot be used on a datatype of "${dataType}".`
);
}
})();
const givenSize = Number.parseInt(sizeValue, 10) || 0;
return { valid: givenSize >= size };
}
},
max: {
message: (
params: BuiltInValidatorMessageGetterParams<ValidatorRuleMaxParams>
) => {
const { propKey, dataType, ruleParams } = params;
const { size } = ruleParams;
if (dataType === "string") {
return `${formatPropKey(
propKey
)} must have no fewer than ${size} characters`;
} else if (dataType === "number") {
return `${formatPropKey(propKey)} must be no smaller than ${size}`;
} else if (dataType === "array" || dataType === "object") {
return `${formatPropKey(
propKey
)} must have no fewer than ${size} items`;
} else {
throw new Error(
`The rule "max" cannot be used on a datatype of "${dataType}".`
);
}
},
fn: (params: BuiltInValidatorParams<ValidatorRuleMinParams>) => {
const { value, dataType, ruleParams } = params;
const { size } = ruleParams;
const sizeValue = (() => {
if (dataType === "string") {
return value.length;
} else if (dataType === "number") {
return value;
} else if (dataType === "array") {
return value.length;
} else if (dataType === "object") {
return Object.keys(value).length;
} else {
throw new Error(
`The rule "max" cannot be used on a datatype of "${dataType}".`
);
}
})();
const givenSize = Number.parseInt(sizeValue, 10) || 0;
return { valid: givenSize <= size };
}
},
pattern: {
message: (
params: BuiltInValidatorMessageGetterParams<ValidatorRulePatternParams>
) => {
const { propKey, dataType, ruleParams } = params;
const { pattern } = ruleParams;
return `${formatPropKey(
propKey
)} must match the pattern "${pattern.toString()}"`;
},
fn: (params: BuiltInValidatorParams<ValidatorRulePatternParams>) => {
const { value, dataType, ruleParams } = params;
const { pattern } = ruleParams;
return { valid: pattern.test(value || "") };
}
},
};
// ############################################################################
// Check value
// ############################################################################
interface CheckSingleValueProps<T extends ValidatorFieldDataType> {
value: any;
descriptor: PickValidatorFieldDescriptor<T>;
propKey?: string;
validationEnabled?: boolean;
auxData?: any;
}
function checkValue<T extends ValidatorFieldDataType>({
value,
descriptor,
propKey = "thisValue",
validationEnabled,
auxData
}: CheckSingleValueProps<T>): PickValidatorFieldResult<T> {
const isBoolean = typeof value === "boolean";
const hasBoolean =
descriptor.type === "boolean" && (value === false || value === true);
const isString = typeof value === "string";
const hasString =
descriptor.type === "string" &&
typeof value === "string" &&
value.length > 0;
const isNumber = typeof value === "number";
const hasNumber = descriptor.type === "number" && typeof value === "number";
const isArray = Array.isArray(value);
const hasArray = descriptor.type === "array" && Array.isArray(value);
const isObject = utilFns.isObject(value);
const hasObject = descriptor.type === "object" && utilFns.isObject(value);
const isPrimitive = isBoolean || isString || isNumber;
const hasValue =
hasBoolean || hasString || hasNumber || hasArray || hasObject;
if (descriptor.type === "boolean") {
if (descriptor.required && !hasBoolean) {
const out: ValidatorFieldPrimativeResult = {
messages: [builtInSystemChecks.required(propKey)],
isValid: false
};
return out as PickValidatorFieldResult<T>;
}
if (!isBoolean) {
const out: ValidatorFieldPrimativeResult = {
messages: [
builtInSystemChecks.incorrectFormat(propKey, "boolean", typeof value)
],
isValid: false
};
return out as PickValidatorFieldResult<T>;
}
}
if (descriptor.type === "string") {
if (descriptor.required && !hasString) {
const out: ValidatorFieldPrimativeResult = {
messages: [builtInSystemChecks.required(propKey)],
isValid: false
};
return out as PickValidatorFieldResult<T>;
}
if (!isString) {
const out: ValidatorFieldPrimativeResult = {
messages: [
builtInSystemChecks.incorrectFormat(propKey, "string", typeof value)
],
isValid: false
};
return out as PickValidatorFieldResult<T>;
}
}
if (descriptor.type === "number") {
if (descriptor.required && !hasNumber) {
const out: ValidatorFieldPrimativeResult = {
messages: [builtInSystemChecks.required(propKey)],
isValid: false
};
return out as PickValidatorFieldResult<T>;
}
if (!isNumber) {
const out: ValidatorFieldPrimativeResult = {
messages: [
builtInSystemChecks.incorrectFormat(propKey, "number", typeof value)
],
isValid: false
};
return out as PickValidatorFieldResult<T>;
}
}
if (descriptor.type === "array") {
if (descriptor.required && !hasArray) {
const out: ValidatorFieldArrayResult = {
messages: [builtInSystemChecks.required(propKey)],
isValid: false,
selfIsValid: false,
childrenAreValid: true,
children: []
};
return out as PickValidatorFieldResult<T>;
}
if (!isArray) {
const out: ValidatorFieldArrayResult = {
messages: [
builtInSystemChecks.incorrectFormat(propKey, "array", typeof value)
],
isValid: false,
selfIsValid: false,
childrenAreValid: true,
children: []
};
return out as PickValidatorFieldResult<T>;
}
}
if (descriptor.type === "object") {
if (descriptor.required && !hasObject) {
const out: ValidatorFieldObjectResult = {
messages: [builtInSystemChecks.required(propKey)],
isValid: false,
selfIsValid: false,
childrenAreValid: true,
children: {}
};
return out as PickValidatorFieldResult<T>;
}
if (!isObject) {
const out: ValidatorFieldObjectResult = {
messages: [
builtInSystemChecks.incorrectFormat(propKey, "object", typeof value)
],
isValid: false,
selfIsValid: false,
childrenAreValid: true,
children: {}
};
return out as PickValidatorFieldResult<T>;
}
}
function mapRules(rules: any, output: any) {
utilFns.ensureArray(rules).forEach((rule) => {
const result = builtInValidators[rule.type].fn({
value,
dataType: descriptor.type,
ruleParams: rule.params
});
if (!result.valid) {
const message = builtInValidators[rule.type].message({
propKey,
dataType: descriptor.type,
ruleParams: rule.params,
validityInfo: result.validityInfo
});
output.messages.push(message);
output.isValid = false;
}
});
}
if (
descriptor.type === "boolean" ||
descriptor.type === "number" ||
descriptor.type === "string"
) {
const output: ValidatorFieldPrimativeResult = {
messages: [],
isValid: true
};
if (hasValue) {
mapRules(descriptor.rules, output);
}
return output as PickValidatorFieldResult<T>;
} else if (descriptor.type === "array") {
const output: ValidatorFieldArrayResult = {
isValid: true,
selfIsValid: true,
childrenAreValid: true,
messages: [],
children: []
};
if (hasValue) {
mapRules(descriptor.rules, output);
if (!output.isValid) output.selfIsValid = false;
output.children = value.map((childValue: any) => {
const result = checkValue({
value: childValue,
descriptor: descriptor.children
});
if (!result.isValid) {
output.childrenAreValid = false;
output.isValid = false;
}
return result;
});
}
return output as PickValidatorFieldResult<T>;
} else if (descriptor.type === "object") {
const output: ValidatorFieldObjectResult = {
isValid: true,
selfIsValid: true,
childrenAreValid: true,
messages: [],
children: {}
};
if (hasValue) {
mapRules(descriptor.rules, output);
if (!output.isValid) output.selfIsValid = false;
output.children = Object.fromEntries(
Object.entries(descriptor.children).map((ruleEntry) => {
const [childKey, childDescriptor] = ruleEntry as [
string,
AnyValidatorFieldDescriptor
];
const childValue = value[childKey];
const result = checkValue({
value: childValue,
descriptor: childDescriptor,
propKey: childKey
});
if (!result.isValid) {
output.childrenAreValid = false;
output.isValid = false;
}
return [childKey, result];
})
);
}
return output as PickValidatorFieldResult<T>;
} else {
throw new Error("Value was not primitive, array, or object.");
}
throw new Error(`Invalid descriptor type: "${descriptor.type}"`);
}
// ############################################################################
// Validate one record
// ############################################################################
// function checkRecord<Rec, Rules>({
// record,
// rules,
// auxData,
// validationEnabled = true,
// }: UseValidatedRecordProps): UseValidatedRecordData {
// }
// ############################################################################
// Validate many records
// ############################################################################
// function checkRecords<Rec, Rules>({
// records,
// rules,
// auxData,
// validationEnabled,
// }: UseValidatedRecordsProps): UseValidatedRecordsData {
// }
// ############################################################################
// Output
// ############################################################################
export const validator = {
bivs: builtInValidators,
biscs: builtInSystemChecks,
checkValue,
utils: {}
};
正如我所说,实现功能很好。问题是我编写的机制(使用可区分的合并)不起作用。因此,这样的行会引发 TS 错误:
线:
expect(result.children[1].children.name.isValid).toBe(true)
错误:
Property 'children' does not exist on type 'ValidatorFieldPrimativeResult | ValidatorFieldArrayResult | ValidatorFieldObjectResult'.
这是因为 TS 认为验证结果始终是 3 种结果类型之一,而我希望 TS 根据我对文本成员和语句的使用来缩小范围。if
整个代码太长,无法在此处发布(但请参阅完整代码的链接),但我会发布我描述的部分以保持这篇文章的完整性:
if (descriptor.type === 'boolean' || descriptor.type === 'number' || descriptor.type === 'string') {
const output: ValidatorFieldPrimativeResult = {
messages: [],
isValid: true,
};
if (hasValue) {
mapRules(descriptor.rules, output);
}
return output as PickValidatorFieldResult<T>;
} else if (descriptor.type === 'array') {
const output: ValidatorFieldArrayResult = {
isValid: true,
selfIsValid: true,
childrenAreValid: true,
messages: [],
children: [],
};
if (hasValue) {
mapRules(descriptor.rules, output);
if (!output.isValid) output.selfIsValid = false;
output.children = value.map((childValue: any) => {
const result = checkValue({
value: childValue,
descriptor: descriptor.children,
});
if (!result.isValid) {
output.childrenAreValid = false;
output.isValid = false;
}
return result;
});
}
return output as PickValidatorFieldResult<T>;
} else if (descriptor.type === 'object') {
const output: ValidatorFieldObjectResult = {
isValid: true,
selfIsValid: true,
childrenAreValid: true,
messages: [],
children: {},
}
if (hasValue) {
mapRules(descriptor.rules, output);
if (!output.isValid) output.selfIsValid = false;
output.children = Object.fromEntries(
Object.entries(descriptor.children).map(ruleEntry => {
const [childKey, childDescriptor] = ruleEntry as [string, AnyValidatorFieldDescriptor];
const childValue = value[childKey];
const result = checkValue({
value: childValue,
descriptor: childDescriptor,
propKey: childKey,
});
if (!result.isValid) {
output.childrenAreValid = false;
output.isValid = false;
}
return [childKey, result];
})
);
}
return output as PickValidatorFieldResult<T>;
} else {
throw new Error('Value was not primitive, array, or object.');
}
我真正想避免的是不得不做这样的事情(这将在我使用验证器的许多地方重复)
(((result as ValidatorFieldArrayResult).children[0] as ValidatorFieldObjectResult).children.name as ValidatorFieldPrimativeResult).messages.length
不幸的是,由于验证遍历的无限递归和多管齐下的分支,我无法在调用时传递类型。
我唯一能想到的另一件事就是使用,但肯定有更好的方法!any
答: 暂无答案
评论