Owen Tsai
Owen Tsai

Reputation: 31

Vue 3 Typescript props validation at both compile and runtime

My team is building a Vue 3 component library. As the project goes larger in scale, we refactored it with typescript to get a better developing experience.

But here's the thing - there are teams using our lib in different environments, with or without typescript and bundler, which means for almost every component we need to validate prop types both in compile-time and runtime.

props: {
  someProp: {
    type: String as PropType<'enum1' | 'enum2' | 'enum3'>,
    validator: (v: string) => {
      return ['enum1', 'enum2', 'enum3'].includes(v)
    }
  }
}

The code runs well. Teams using typescript are happy - they got their code completion and can spot potential errors ahead of time. Teams using javascript are happy - they can discover any type-related errors at runtime thanks to the validator. But we are not happy, since the code is duplicated. Technically <'enum1' | ...> is a type def and ['enum1', ...] is an array, they are not the same thing - but we literally write the same string multiple times.

Is there any way to implement something that can define a prop with PropType and validator without having to reeeeeeepeat myself?

Upvotes: 2

Views: 940

Answers (1)

Owen Tsai
Owen Tsai

Reputation: 31

Well, currently I'm doing something like this:

export const buildProp = <
  T = any,
  R extends boolean = boolean,
  D extends T = T
>({
  type?: any,
  /* all the values allowed for this prop */
  values?: readonly T[],
  required?: R,
  /* if a prop is required, it shouldn't default to any value */
  /* if a prop has a default value, it shouldn't be marked as required */
  defaultValue?: R extends true
    ? never
    : D extends Record<string, unknown> | Array<any>
      ? () => D
      : D,
  /* any other validations */
  validator?: (val: any) => boolean
} = {}) => {
  return {
    type: type as PropType<T | never>,
    required: !!required,
    default: defaultValue,
    validator: (val: any) => {
      let valid = false

      // we see if the prop value passed in is in the union
      // or is the defaultValue
      if (values) {
        valid =|| [...values, defaultValue].includes(val)
      }
      if (validator) {
        valid =|| validator(val)
      }

      return valid
    }
  }
}

As a result,

someProp: buildProp({
  type: String,
  values: ['enum1', 'enum2', 'enum3']
})

will produce

someProp: {
  type: String as PropType<'enum1' | 'enum2' | 'enum3'>,
  validator: (val) => {
    return ['enum1', 'enum2', 'enum3'].includes(val)
  }
}

if (props.someProp === 'illegalValue') {
  // ERR: will always return false since type "'enum1' | 'enum2' | 'enum3'" has no overlap with "illegalValue"
}

Upvotes: 1

Related Questions