◆

Vue vue-types 基础用法与 Props 类型校验

2023-02-02 01:370

Vue 原生的 props 声明写起来很啰嗦:一个带默认值的必填字符串要写成 { type: String, required: true, default: 'x' },想限定只能取几个特定值还得自己写 validator。而且原生 type: String 在 TS 里只能推出 string,没法表达「只能是 'apple' | 'pear'」这种联合类型。

vue-types 用链式调用把这些收敛成一行:string().def('x')、number<1|2|3>().def(1),同时补上了 TS 泛型标注能力——泛型管编译期类型检查,def/isRequired/validate 管运行时校验,两套机制各管一段。

这篇是基础类型的用法验证。代码里保留了大量被注释掉的错误示例和对应的 TS 报错信息,这些反例比正例更有价值,能直接看出哪些写法编译期就会被拦下、哪些要到运行时才报错、以及少数「TS 报错但实际能跑」的边界情况。

基础使用示例如下

这段把 number、string、bool、func、object、array、any、symbol、integer 逐个走了一遍。几处值得留意的地方:

  • 两种调用风格等价:VueTypes.number.def(10) 是属性式,number().def(15) 是函数式;带泛型时必须用函数式。
  • 默认值差异:integer 自带默认值 0,而 number() 没有默认值。
  • 泛型收窄是编译期能力:number<1|2|3>().def(4) 会被 TS 直接拦住,运行时并不会校验这个范围。
  • 枚举那几条是个坑:string<Fruits>().def(Fruits.Apple) 运行时会报错,反而写字面量 'Apple' 能跑,代价是 TS 报类型不匹配。
  • object().def([]) 传数组会被运行时校验拦下(控制台输出 [VueTypes warn]: object - invalid default value),因为数组不是合法的对象默认值。
  • func().def(console.log(1)) 传的是调用结果 undefined 而不是函数,TS 会报错;正确写法是 .def(() => console.log(1))。
  • 最后的 validate 是自定义运行时校验,回调返回布尔值决定是否通过。

底部把 props 解构出来塞进 reactive 数组渲染,是为了在页面上直接看到每个 prop 最终拿到的值——尤其是没传值时默认值到底是什么。注意解构 props 会丢失响应性,这里只为展示初始值,实际业务代码里不该这么写。

<template>
  <div v-for="(item, index) in testArrs" :key="index">
    <div>{{ item.name }}:{{ item.value }}</div>
  </div>
</template>

<script lang="ts" setup>
import VueTypes, {
  number,
  string,
  integer,
  any,
  array,
  bool,
  func,
  object,
  symbol,
} from 'vue-types';
import { reactive } from 'vue';

interface Users {
  id: number;
  username: string;
}

enum Fruits {
  Apple = 'Apple',
  Pear = 'Pear',
}

const props = defineProps({
  /** 直接使用引入的 VueTypes 测试 */
  numberPriDef: VueTypes.number.def(10),
  stringEmpty: VueTypes.string,
  /** 声明了 isRequired 代表必传 否则控制台会抛出一个错误提示:Missing required prop: "stringReqDef"  */
  // stringReqDef: VueTypes.string.isRequired.def('123'),
  /** integer 的默认值是 0 */
  integerEmpty: VueTypes.integer,

  numberDef: number().def(15),
  /** number 类型没有默认值 */
  numberEmpty: number(),
  /** 必须是 1、2、3 中的某一个 */
  numberLimit: number<1 | 2 | 3>().def(1),
  /** TS2345: Argument of type '4' is not assignable to parameter of type '1 | 2 | 3 | undefined'. */
  // numberLimitOne: number<1|2|3>().def(4),

  stringDef: string().def('testString'),
  /** 枚举 */
  stringEnum: string<Fruits>(),
  /** 下面这种写法在运行时会直接报错 */
  // stringEnumOne: string<Fruits>().def(Fruits.Apple),
  /**
   * TS2345: Argument of type '"Apple"' is not assignable to parameter of type 'Fruits | undefined'.
   * 但实际上运行不会有问题
   */
  stringEnumTwo: string<Fruits>().def('Apple'),

  /** 联合类型 */
  stringUnion: string<'apple' | 'pear'>(),
  stringUnionOne: string<'apple' | 'pear'>().def(undefined),
  stringUnionTwo: string<'apple' | 'pear'>().def('apple'),
  /** TS2345: Argument of type '"apples"' is not assignable to parameter of type '"apple" | "pear" | undefined'. */
  // stringUnionThree: string<'apple'|'pear'>().def('apples'),

  boolEmpty: bool(),
  uniq: symbol(),

  funcEmpty: func(),
  /** TS2345: Argument of type 'void' is not assignable to parameter of type '((...args: any[]) => any) | undefined'. */
  // funcEmptyOne: func().def(console.log(1)),
  funcEmptyTwo: func().def(() => console.log(1)),
  funcEmptyThree: func().def(undefined),
  funcParams: func<(event: Event) => void>().def(() => 1),
  funcParamsOne: func<(event: string) => void>().def(() => {
    return 1;
  }),

  anyDef: any().def(null),
  anyKnown: any<unknown>(),

  obj: object().def({}),
  /** 控制台抛出错误:[VueTypes warn]: object - invalid default value: "" */
  // objOne: object().def([]),
  /** TS2345: Argument of type '1' is not assignable to parameter of type 'DefaultFactory{ [key: string]: any; }> | */
  // userObjTwo: object().def(1),
  objThree: object().def({ a: 1 }),
  objFour: object<Users>().def({ id: 1, username: 'wfd' }),
  /** Argument of type '{ id: number; username: string; age: number; }' is not assignable to parameter of type 'DefaultFactory<Users> | undefined'. */
  // objFive: object<Users>().def({id: 1, username: 'wfd', age: 33}),

  arrEmpty: array<string>(),
  arrEmptyOne: array<string>().def(['11', '22']),
  /** TS2322: Type 'number' is not assignable to type 'string'. */
  // arrEmptyTwo: array<string>().def(['11', 22]),
  /** 指定允许的数组项 */
  arrOne: array<'apple' | 'pear'>().def(['pear', 'apple']),
  /** TS2322: Type '"egg"' is not assignable to type '"apple" | "pear"'. */
  // arrTwo: array<'apple'|'pear'>().def(['pear', 'apple', 'egg']),

  /** 自定义验证器 */
  testValidate: string()
    .def('11')
    .validate((value: unknown | string) => {
      return (value as string).includes('1');
    }),
});

const {
  numberPriDef,
  stringEmpty,
  integerEmpty,
  numberDef,
  numberEmpty,
  numberLimit,
  stringDef,
  stringEnum,
  stringUnion,
  boolEmpty,
  uniq,
  funcEmpty,
  funcParams,
  anyDef,
  anyKnown,
} = props;

const testArrs = reactive([
  { name: 'numberPriDef', value: numberPriDef },
  { name: 'stringEmpty', value: stringEmpty },
  { name: 'integerEmpty', value: integerEmpty },
  { name: 'numberDef', value: numberDef },
  { name: 'numberEmpty', value: numberEmpty },
  { name: 'numberLimit', value: numberLimit },
  { name: 'stringDef', value: stringDef },
  { name: 'stringEnum', value: stringEnum },
  { name: 'stringUnion', value: stringUnion },
  { name: 'boolEmpty', value: boolEmpty },
  { name: 'uniq', value: uniq },
  { name: 'funcEmpty', value: funcEmpty },
  { name: 'funcParams', value: funcParams },
  { name: 'anyDef', value: anyDef },
  { name: 'anyKnown', value: anyKnown },
]);
</script>

<style scoped></style>
💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVEVue vue-types 基础用法与 Props 类型校验加载中