vue props使用typescript自定义类型的方法实例

前言

Base: vue@3.2.33 + typescript@4.1.6 + npm@8.5.0

尝试解决将ts中自定义的interface/type,传vue的props属性的问题。

记录一下过程和思路。

一、问题定位

官方文档中说,props自定义类型检查是可能的。

In addition, type can also be a custom class or constructor function and the assertion will be made with an instanceof check. For example, given the following class:
https://vuejs.org/guide/components/props.html#boolean-casting

但注意到,文档给出的支持类型为class或者constructor function。

官方给的example也是基于class的

class Person {
 constructor(firstName, lastName) {
 this.firstName = firstName
 this.lastName = lastName
 }
}

export default {
 props: {
 author: Person
 }
}

但如果使用ts中的interface,就会报错'Person' only refers to a type, but is being used as a value here.

见下面代码:

interface Person {
 firstName: string
 lastName: string
}

//错误用法
export default {
 props: {
 author: Person
 }
}

归根结底是vue源码里的定义方式

declare interface PropOptions<T = any, D = T> {
 type?: PropType<T> | true | null;
 required?: boolean;
 default?: D | DefaultFactory<D> | null | undefined | object;
 validator?(value: unknown): boolean;
}

export declare type PropType<T> = PropConstructor<T> | PropConstructor<T>[];

declare type PropConstructor<T = any> = {
 new (...args: any[]): T & {};
} | {
 (): T;
} | PropMethod<T>;

declare type PropMethod<T, TConstructor = any> = [T] extends [
((...args: any) => any) | undefined
] ? {
 new (): TConstructor;
 (): T;
 readonly prototype: TConstructor;
} : never;

对type属性的检查是PropType<T>

一路查找定义到PropConstructor<T>

可以看到PropConstructor的定义

{ new ( ..args: any[]): T & {} } ,class类型。拥有“接受任何参数并返回指定类型的构造函数”的一个class。{ (): T } 函数类型,一个无参传入并返回指定类型的函数。PropMethod<T>类型。

其中, PropMethod<T> 类型使用了extends条件类型,用于包裹T为任意函数类型or undefined的情况,此时会需要一个含有构造函数的函数类型,这个函数类型的实例在调用时会返回T。

总结一下,PropConstructor<T>的要求无非就是

要么你是class,构造函数返回T,

要么传入一个无参函数返回T。

要么把这个无参函数包裹在一个有构造函数的class里(用PropMethod泛型)

二、初级解决方案

上面部分我们已经定位到了问题。

我们本质上是要通过PropType的校验。

解法一,函数法

参照PropConstructor的要求写就可以了。

用一个 ()=>T 类型的函数为type赋值,通过校验。

interface Person {
 firstName: string
 lastName: string
}

const PersonTypeHelper = function (): Person {
 return {} as Person
}

export default {
 props: {
 author: {
 type: PersonTypeHelper
 }
 }
}

上面这个思路可以继续优化

//更简单一点不写实现
declare var PersonType : ()=> Person

export default {
 props: {
 author: {
 type: PersonType 
 }
 }
}

这样仍然很麻烦,不能为每个自定义interface 都写一个func / var吧。

我们可以继续优化,写一个更通用的var。

declare var commonType: ()=> any

export default {
 props: {
 author: {
 //使用时把Person修改成其他自定义类型即可
 type: commonType as ()=> Person 
 }
 }
}

这样写能过ide的类型推导,但是run起来会报错 ReferenceError: PersonType is not defined
当然这并不重要。因为就算写了实现也不会通过类型校验。
我会在下个章节解决类型校验问题,此处先展示思路。

解法二 PropType泛型

上面这个思路,需要每次声明一个commonType变量。

对某些库函数党来说,可能不太舒服。

既然思路是绕过PropType的校验,直接使用PropType不是最直观的吗?

interface Person {
 firstName: string
 lastName: string
}

import type { PropType } from "vue"

export default {
 props: {
 author: {
 type: Object as PropType<Person> 
 // type: {} as PropType<Person>
 }
 }
}

这样做其实跟使用commonType没啥本质区别。

因为import type {PropType}也需要一行。

声明一个commonType也需要一行。

我们其实也可以使用PropMethod泛型,但是由于vue没有做直接export,日常使用不太方便。

上面两个解法都能过ide类型推导,但是runtime时有点问题。

三、props的校验过程

前面的解法,在运行过程中可能会报警告,类型校验不通过。
[Vue warn]: Invalid prop: type check failed for prop "formItems". Expected , got Array

为了解决这个警告。

我们来读一下校验类型的源码。

传入的value是实际获得的对象,type是我们在 {type: Person}里传入的值。

function assertType(value, type) {
 let valid;
 const expectedType = getType(type);
 if (isSimpleType(expectedType)) {
 const t = typeof value;
 valid = t === expectedType.toLowerCase();
 // for primitive wrapper objects
 if (!valid && t === 'object') {
 valid = value instanceof type;
 }
 }
 else if (expectedType === 'Object') {
 valid = isObject(value);
 }
 else if (expectedType === 'Array') {
 valid = isArray(value);
 }
 else if (expectedType === 'null') {
 valid = value === null;
 }
 else {
 valid = value instanceof type;
 }
 return {
 valid,
 expectedType
 };
}

const isSimpleType = /*#__PURE__*/ makeMap('String,Number,Boolean,Function,Symbol,BigInt');

// use function string name to check type constructors
// so that it works across vms / iframes.
function getType(ctor) {
 const match = ctor && ctor.toString().match(/^\s*function (\w+)/);
 return match ? match[1] : ctor === null ? 'null' : '';
}

前面的代码就是区分简单数据类型,对象、数组,判空。

最后一个是,如果type不在上述内置类型中,就使用 value instanceof type 来判断。

这里的关键在于getType函数。

vue的getType函数,假定我们传入的是一个constructor,

假定我们的constructor写法是

function ctorName(){ ...}

试图捕获这个ctorName。

如果不这么写, 返回的expectedType名称就是空字符串。

这解释了,如果我们仅仅读了文档,老老实实地按文档说的,传入了一个构造函数,会报错。

class Person{
	constructor (){...}
}

props:{
	type: Person.constructor
}

因为传入 Person.constructor在vue内部会被解析成getType函数匹配为 ‘Function’ 类型。

构造函数没有名字,默认名字是function Function(){ somecode}

于是发生类型不匹配

TS2769: No overload matches this call.
  The last overload gave the following error.
    Type '{ type: Function; }' is not assignable to type 'Prop<unknown, unknown> | null'.
      Types of property 'type' are incompatible.
        Type 'Function' is not assignable to type 'true | PropType<unknown> | null | undefined'.
          Type 'Function' is missing the following properties from type 'PropConstructor<unknown>[]': pop, push, concat, join, and 28 more.

当然这个不重要,重要的是 valid = value instanceof type 这句。

instanceof 关键词的原理是不断回溯左值的__proto__,直到其找到一个等于 右值.prototype的原型,就返回true。遍历到根 null就返回false。

function instanceof(L, R) {
 // L为左值,R为右值
 var R = R.prototype
 var L = L.__proto__
 while (true) {
 if (L === null) {
 return false
 }
 if (L === R) {
 return true
 }
 L = L.__proto__
 }
}

在上述例子中,校验失败的核心原因在于。

如果我们传入一个匿名函数 ()=> T 作为type右值。

由于匿名函数是没有prototype的,

所以传入任意value instanceof anonymous 会返回false。

如果我们采用PropType<T>来写也有问题,

{
	type: Object as PropType<Person> 
}

右值中的类型会变成Object。

于是在instanceof比较的时候 , R.prototype === Object.prototype。

因此传入任意非空数据都会通过vue的校验

因为任意非空数据遍历 __proto__都会来到Object.prototype。

所以上面两个解法,其实本质上是,

无论传入什么都会报警告和无论传入什么都不报警告的区别。

四、后话

我试了另外几种思路,由于interface只在编译时起作用,本身并不能设置prototype。

所以无论怎么折腾interface / type 的泛型,都不太好解决这个问题。

目前来看的一种折中方式是使用class。

class有prototype。

但是需要严格地约定,在传入端也使用class的constructor构造数据。

这样才能将原型存进数据里。

但是这种做法对一些小型接口其实并不友好。

例如

export interface Person {
 name: string
 age: number
}

用class的话就非得改成

export class Person {
 name: string
 age: number
 constructor(n,a) {
 this.name = n
 this.age = a
 }
}

然后在传入的地方使用构造函数。

import {Person} from "./types"
const data = new Person("aa",123)

这显然不如我们直接使用对象字面量方便。

const data = {name:"aa", age:123}

这归根结底是因为,instanceof是基于原型校验,而非值校验的。

使用对象字面量并不能通过校验。

当然了,vue其实是支持我们写一个自定义的validator()的。

上面的探索只是想尝试绕过这个步骤。

custom validator相关源码。

function validateProp(name, value, prop, isAbsent) {
 const { type, required, validator } = prop;
 // required!
 if (required && isAbsent) {
 warn('Missing required prop: "' + name + '"');
 return;
 }
 // missing but optional
 if (value == null && !prop.required) {
 return;
 }
 // type check
 if (type != null && type !== true) {
 let isValid = false;
 const types = isArray(type) ? type : [type];
 const expectedTypes = [];
 // value is valid as long as one of the specified types match
 for (let i = 0; i < types.length && !isValid; i++) {
 const { valid, expectedType } = assertType(value, types[i]);
 expectedTypes.push(expectedType || '');
 isValid = valid;
 }
 if (!isValid) {
 warn(getInvalidTypeMessage(name, value, expectedTypes));
 return;
 }
 }
 // custom validator
 if (validator && !validator(value)) {
 warn('Invalid prop: custom validator check failed for prop "' + name + '".');
 }
}

需要注意的是,vue这里做了double check。

如果有传入type,先在type做一次校验。

通过type校验后,再看有没有validator,如果有额外做一次。

这提示我们,

对于复杂的自定义数据类型,

type本身不能成为校验工具,最好不写,减少一次运算。

export default {
 props: {
 author: {
 validator: function (value: Person) {
 return typeof value.name === "string" && typeof value.age === "number"
 }
 }
 }
}

参考

  • //https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#inferring-within-conditional-types
  • //https://frontendsociety.com/using-a-typescript-interfaces-and-types-as-a-prop-type-in-vuejs-508ab3f83480
  • //https://blog.csdn.net/qq_34998786/article/details/120300361

总结 

作者:w55100原文地址:https://blog.csdn.net/w55100/article/details/124624300

%s 个评论

要回复文章请先登录注册