js 数据类型校验

431 阅读5分钟

需求背景

目前开发的标注项目, 用户先配置工具,资源和预识别数据生成任务, 然后进入任务开始标注作业.

遇到的问题:

  1. 预识别的数据格式和标注模版需要的数据结构不一致,导致模版功能无法正常使用.
  2. 用户标注作用过程中,因为某些原因(比如报错导致部分逻辑中止),导致数据结构错误.
  3. 标注作业保存的标注结果错误,任务流转的时候,可能导致的丢框问题.

每次进入标注任务和保存的时候,校验数据,能更快的定位到问题发生的位置.如果没有校验,用户没办法感知到出现里问题,有问题的数据流转下去,等发现问题的时候,排查日志就需要很大的成本.

npm 及 git 地址

check-necessary-fields - npm (npmjs.com)

mingju-90/check-necessary-fields: 校验数据类型(只校验定义的字段类型) (github.com)

实现过程

递归遍历对象,对比需要校验的数据

刚开始的时候想通过遍历对象,判断期望的数据类型和需要校验的数据,可是如果需要校验联合类型的时候,就会有问题产生。

const line = {
    type: 'line',
    coordinates: [['number']]
}
const point = {
    type: 'point',
    coordinates: ['number']
}

const geometry // 为 line 或者 point 的时候, 就没办法校验了

将传入的数据(需要根据一定的规则定义)进行转化成更方便操作的格式,然后进行校验

interface checkData {
    type: string | string[],   // union 代表当前类型为联合类型,只要 value 中有一个类型符合就为true; object 代表当前类型为对象,需要 value 中每个字段都符合才为 true; 字符串数组代表当前类型为基础数据类型或者为指定字符串,只要符合其中一个为 true; array 代表当前为数组,需要每个元素都符合 value 类型;
    key: string,  // 表示需要校验对象的 key
    value: undefined | checkData | checkData[]  // undefined 表示当前数组没有制定元素类型;checkData 为数组元素的类型; checkData[] 为对象字段类型
}

遇到的问题,如果定义的类型是递归类型,比如,小明是 People 的实例,他有个属性 friends, 为 People[] 类型,这个时候转换为校验类型的时候,就会报错,所以需要记录已经转换过的数据。

这种类型的数据

class People {
    name: string
    age: number
    friends: People[]
    constructor({name, age}) {
        this.name = name
        this.age = age
        this.friends = []
    }
}

这个时候, people已经转换过,所以转换friends的时候,就需要终止递归.

const People = {
    name: 'string',
    age: 'number'
}
People.friends = [People]

将数据类型转换为方便校验的对象结构,为了处理递归类型,需要保存已经转换过的对象

const transformationType = (data, key = "", map = new Map()) => {
    if (typeof data === "object" && map.has(data)) return map.get(data);
    const result = { key, type: "" };
    map.set(data, result);
    const type = getType(data);
    if (type === "string") {
        result.type = data.split("|").map((item) => item.trim());
    } else if (type === "object") {
        result.type = "object";
        result.value = Object.keys(data).map((key) => {
            return transformationType(data[key], key, map);
        });
    } else if (type === "array") {
        result.type = "array";
        result.value = data[0]
            ? transformationType(data[0], "", map)
            : undefined;
    } else if (type === "function") {
        const functionResult = data();
        result.type = "union";
        result.value = functionResult.map((item) => {
            return transformationType(item, "", map);
        });
    } else {
        result.type = [data];
    }
    
    return result;
};

获取数据类型,数字类型需要特别校验 NaN,Infinity

当校验数字类型的时候, 大部分时候希望校验的是正常的数据, NaN, Infinity 希望不能通过,所以细分了数字类型

  • number
  • NaN
  • Infinity
  • -Infinity
let getType = (data) =>
Object.prototype.toString
  .call(data)
  .match(/\[object (.*)\]/)[1]
  .toLocaleLowerCase();

const numberType = (type, data) => {
    if (type !== "number") return type;
    if (Number.isNaN(data)) return "NaN";
    if (data > Number.MAX_SAFE_INTEGER) return "Infinity";
    if (data < Number.MIN_SAFE_INTEGER) return "-Infinity";
    return type;
};

const afterFn = (target, after) => {
    return (...args) => {
        const result = target(...args);
        return after(result, ...args);
    };
};

getType = afterFn(getType, numberType);

校验数据的方法

const check = (checkData, data) => {
    const dataType = getType(data);
    // 如果是 type 是数组, 代表 data 应该是基础数据类型, 直接判断
    
    let result = false;
    if (getType(checkData.type) === "array") {
        result = checkData.type.includes(dataType) || checkData.type.includes(data);
    } else if (checkData.type === "union") {
        result = checkData.value.some((item) => check(item, data));
    } else if (checkData.type !== dataType) {
        return false;
    } else if (dataType === "object") {
        result = checkData.value.every((item) => check(item, data[item.key]));
    } else if (dataType === "array") {
        result = checkData.value
            ? data.every((item) => check(checkData.value, item))
            : true;
    }
    
    return result;
};
const checkNecessaryFields = (type, data) => {
    const typeData = transformationType(type);
    return check(typeData, data);
};

使用方法

/**
 * 校验数据类型, 返回布尔值
 * @param {*} type 期望的数据类型
 * @param {*} data 需要被校验的数据
 * @returns {boolean}
 */
checkNecessaryFields(type, data)  // boolean

基本数据类型校验

  • 字符串: 'string'。
  • 数字: 'number', 'NaN', '-Infinity', 'Infinity', 日常使用中,指定数字类型,一般不希望为NaN等,所以做了更细致的类型区分。
  • undefined: 'undefined'
  • null: 'null'
  • boolean: 'boolean'
  • 联合基本数据类型: 'string | number | null', 将需要校验的数据类型通过 | 分隔。
  • 指定值: () => [1, '1', false, ''], 校验数据是否是指定的某个值。
  • 制定字符串: 'str | name | 123', 校验数据是否为指定的字符串,将多个字符串通过 | 分隔。

引用类型校验

  • 基本类型数组: ['string'], 数组的类型为第一个元素定义的类型。
  • 引用类型数组: [{name: 'string'}], [['string | number']], 第一个元素可以为对象或者数组。
  • 联合类型数组: [() => [123, 'string', {name: 'string'}]], 数组中的每个元素可能是不同的类型,通过使用联合类型来定义。

用例

基本类型

checkNecessaryFields('string', 'src') // true
checkNecessaryFields('string', '') // true
checkNecessaryFields('string', '123') // true
checkNecessaryFields('number', '123') // false
checkNecessaryFields('number', 123) // true
checkNecessaryFields('number', 0) // true
checkNecessaryFields('number', NaN) // false

指定值和联合类型

const Point = {
    type: 'Point',
    coordinates: ['number']
}
const Line = {
    type: 'Line'
    coordinates: [
        ['number']
    ]
}

// 点或者线
const PointAndLine = () => [Point, Line]

const point = {
    type: 'Point',
    coordinates: [100, 100]
}

checkNecessaryFields(Point, point) // true
checkNecessaryFields(PointAndLine, point) // true

// 修改点坐标的结构
point.coordinates = [
    [100, 100]
]
checkNecessaryFields(PointAndLine, point) // false

// 修改点的类型为line
point.type = 'Line'
// 先校验点的类型不通过,然后校验线的类型,通过
checkNecessaryFields(PointAndLine, point) // true


// 校验的值必须符合下面几个值
const list = () => [1, 2, '3', false]
checkNecessaryFields(list, 1) // true
checkNecessaryFields(list, '1') // false
checkNecessaryFields(list, '3') // true
checkNecessaryFields(list, '') // false
checkNecessaryFields(list, false) // true

复杂类型

const peopleType = {
    name: 'string',
    age: 'number',
}
peopleType.friends = [peopleType]

const a = {
    name: 'a'
    age: 11,
    friends: [
        {name: 'b', age: 12, friends: []}
    ]
}

checkNecessaryFields(peopleType, a) // true

a.friends = undefined
checkNecessaryFields(peopleType, a) // false

后期调整

  • 返回错误字段得路径: 因为需要校验得数据比较复杂, 单纯得返回布尔值, 不方便找到错误得字段, 所以添加了返回错误字段路径;
  • 调整使用方式: 如果接收得是一个参数, 通过柯里化, 返回一个校验对方结构得函数, 如果接收得是两个参数, 直接返回校验结果;