From 2fdfa2b662c77671586196dcc9913fe0d96b8c8c Mon Sep 17 00:00:00 2001 From: tanjinzhou <415800467@qq.com> Date: Thu, 15 Oct 2020 17:58:05 +0800 Subject: [PATCH] refactor: button date-picker drawer dropdown form by ts --- .../_util/props-util/initDefaultProps.ts | 2 +- components/button/button.tsx | 7 +- components/button/buttonTypes.ts | 2 + components/date-picker/interface.tsx | 2 +- components/drawer/{index.jsx => index.tsx} | 54 ++--- .../drawer/style/{index.js => index.ts} | 0 ...ropdown-button.jsx => dropdown-button.tsx} | 18 +- .../dropdown/{dropdown.jsx => dropdown.tsx} | 9 +- ...etDropdownProps.js => getDropdownProps.ts} | 20 +- components/dropdown/{index.js => index.ts} | 10 +- components/form/{Form.jsx => Form.tsx} | 138 ++++++------ .../form/{FormItem.jsx => FormItem.tsx} | 191 +++++++++-------- components/form/index.jsx | 3 +- components/form/interface.ts | 201 ++++++++++++++++++ .../form/utils/{asyncUtil.js => asyncUtil.ts} | 3 +- .../form/utils/{messages.js => messages.ts} | 0 .../form/utils/{typeUtil.js => typeUtil.ts} | 2 +- .../{validateUtil.js => validateUtil.ts} | 65 ++++-- .../form/utils/{valueUtil.js => valueUtil.ts} | 43 ++-- 19 files changed, 511 insertions(+), 259 deletions(-) rename components/drawer/{index.jsx => index.tsx} (85%) rename components/drawer/style/{index.js => index.ts} (100%) rename components/dropdown/{dropdown-button.jsx => dropdown-button.tsx} (87%) rename components/dropdown/{dropdown.jsx => dropdown.tsx} (94%) rename components/dropdown/{getDropdownProps.js => getDropdownProps.ts} (60%) rename components/dropdown/{index.js => index.ts} (62%) rename components/form/{Form.jsx => Form.tsx} (75%) rename components/form/{FormItem.jsx => FormItem.tsx} (78%) create mode 100644 components/form/interface.ts rename components/form/utils/{asyncUtil.js => asyncUtil.ts} (81%) rename components/form/utils/{messages.js => messages.ts} (100%) rename components/form/utils/{typeUtil.js => typeUtil.ts} (67%) rename components/form/utils/{validateUtil.js => validateUtil.ts} (73%) rename components/form/utils/{valueUtil.js => valueUtil.ts} (53%) diff --git a/components/_util/props-util/initDefaultProps.ts b/components/_util/props-util/initDefaultProps.ts index 8a258c32d..b42c50808 100644 --- a/components/_util/props-util/initDefaultProps.ts +++ b/components/_util/props-util/initDefaultProps.ts @@ -10,7 +10,7 @@ const initDefaultProps = ( : any; }, ): T => { - const propTypes = { ...types }; + const propTypes: T = { ...types } as T; Object.keys(defaultProps).forEach(k => { const prop = propTypes[k] as VueTypeValidableDef; if (prop) { diff --git a/components/button/button.tsx b/components/button/button.tsx index 8d0293904..987c5e0b0 100644 --- a/components/button/button.tsx +++ b/components/button/button.tsx @@ -140,13 +140,14 @@ export default defineComponent({ }, render() { this.iconCom = getComponent(this, 'icon'); - const { type, htmlType, iconCom, disabled, handleClick, sLoading, $attrs } = this; + const { type, htmlType, iconCom, disabled, handleClick, sLoading,href,title, $attrs } = this; const children = getSlot(this); this.children = children; const classes = this.getClasses(); const buttonProps = { ...$attrs, + title, disabled, class: classes, onClick: handleClick, @@ -158,9 +159,9 @@ export default defineComponent({ this.insertSpace(child, this.isNeedInserted() && autoInsertSpace), ); - if ($attrs.href !== undefined) { + if (href !== undefined) { return ( - + {iconNode} {kids} diff --git a/components/button/buttonTypes.ts b/components/button/buttonTypes.ts index a134fbb79..a87eb1191 100644 --- a/components/button/buttonTypes.ts +++ b/components/button/buttonTypes.ts @@ -22,5 +22,7 @@ export default () => ({ ghost: PropTypes.looseBool, block: PropTypes.looseBool, icon: PropTypes.VNodeChild, + href: PropTypes.string, + title: PropTypes.string, onClick: PropTypes.func, }); diff --git a/components/date-picker/interface.tsx b/components/date-picker/interface.tsx index 458b9448e..98a148faa 100644 --- a/components/date-picker/interface.tsx +++ b/components/date-picker/interface.tsx @@ -1,6 +1,6 @@ import moment from 'moment'; -import { CSSProperties, DefineComponent, HTMLAttributes } from 'vue'; +import { CSSProperties, DefineComponent } from 'vue'; import { tuple, VueNode } from '../_util/type'; export type RangePickerValue = diff --git a/components/drawer/index.jsx b/components/drawer/index.tsx similarity index 85% rename from components/drawer/index.jsx rename to components/drawer/index.tsx index b6c43b9f7..94871eddb 100644 --- a/components/drawer/index.jsx +++ b/components/drawer/index.tsx @@ -1,4 +1,4 @@ -import { inject, provide, nextTick } from 'vue'; +import { inject, provide, nextTick, defineComponent, App, CSSProperties } from 'vue'; import classnames from '../_util/classNames'; import omit from 'omit.js'; import VcDrawer from '../vc-drawer/src'; @@ -7,8 +7,11 @@ import BaseMixin from '../_util/BaseMixin'; import CloseOutlined from '@ant-design/icons-vue/CloseOutlined'; import { getComponent, getOptionProps } from '../_util/props-util'; import { defaultConfigProvider } from '../config-provider'; +import { tuple } from '../_util/type'; -const Drawer = { +const PlacementTypes = tuple('top', 'right', 'bottom', 'left'); +type placementType = typeof PlacementTypes[number]; +const Drawer = defineComponent({ name: 'ADrawer', inheritAttrs: false, props: { @@ -22,16 +25,16 @@ const Drawer = { bodyStyle: PropTypes.object, headerStyle: PropTypes.object, drawerStyle: PropTypes.object, - title: PropTypes.any, + title: PropTypes.VNodeChild, visible: PropTypes.looseBool, width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).def(256), height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).def(256), zIndex: PropTypes.number, prefixCls: PropTypes.string, - placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']).def('right'), + placement: PropTypes.oneOf(PlacementTypes).def('right'), level: PropTypes.any.def(null), wrapClassName: PropTypes.string, // not use class like react, vue will add class to root dom - handle: PropTypes.any, + handle: PropTypes.VNodeChild, afterVisibleChange: PropTypes.func, keyboard: PropTypes.looseBool.def(true), onClose: PropTypes.func, @@ -39,22 +42,21 @@ const Drawer = { }, mixins: [BaseMixin], data() { - this.destroyClose = false; - this.preVisible = this.$props.visible; return { - _push: false, + sPush: false, }; }, - setup() { + setup(props) { const configProvider = inject('configProvider', defaultConfigProvider); return { configProvider, + destroyClose: false, + preVisible: props.visible, + parentDrawer: inject('parentDrawer', null) }; }, beforeCreate() { - const parentDrawer = inject('parentDrawer', null); provide('parentDrawer', this); - this.parentDrawer = parentDrawer; }, mounted() { // fix: delete drawer in child and re-render, no push started. @@ -83,7 +85,7 @@ const Drawer = { } }, methods: { - close(e) { + close(e: Event) { this.$emit('update:visible', false); this.$emit('close', e); }, @@ -95,12 +97,12 @@ const Drawer = { // }, push() { this.setState({ - _push: true, + sPush: true, }); }, pull() { this.setState({ - _push: false, + sPush: false, }); }, onDestroyTransitionEnd() { @@ -118,7 +120,7 @@ const Drawer = { return this.destroyOnClose && !this.visible; }, // get drawar push width or height - getPushTransform(placement) { + getPushTransform(placement?: placementType) { if (placement === 'left' || placement === 'right') { return `translateX(${placement === 'left' ? 180 : -180}px)`; } @@ -128,14 +130,14 @@ const Drawer = { }, getRcDrawerStyle() { const { zIndex, placement, wrapStyle } = this.$props; - const { _push: push } = this.$data; + const { sPush: push } = this.$data; return { zIndex, transform: push ? this.getPushTransform(placement) : undefined, ...wrapStyle, }; }, - renderHeader(prefixCls) { + renderHeader(prefixCls: string) { const { closable, headerStyle } = this.$props; const title = getComponent(this, 'title'); if (!title && !closable) { @@ -150,7 +152,7 @@ const Drawer = { ); }, - renderCloseIcon(prefixCls) { + renderCloseIcon(prefixCls: string) { const { closable } = this; return ( closable && ( @@ -161,14 +163,14 @@ const Drawer = { ); }, // render drawer body dom - renderBody(prefixCls) { + renderBody(prefixCls: string) { if (this.destroyClose && !this.visible) { return null; } this.destroyClose = false; const { bodyStyle, drawerStyle } = this.$props; - const containerStyle = {}; + const containerStyle: CSSProperties = {}; const isDestroyOnClose = this.getDestroyOnClose(); if (isDestroyOnClose) { @@ -192,7 +194,7 @@ const Drawer = { }, }, render() { - const props = getOptionProps(this); + const props: any = getOptionProps(this); const { prefixCls: customizePrefixCls, width, @@ -204,7 +206,7 @@ const Drawer = { ...rest } = props; const haveMask = mask ? '' : 'no-mask'; - const offsetStyle = {}; + const offsetStyle: CSSProperties = {}; if (placement === 'left' || placement === 'right') { offsetStyle.width = typeof width === 'number' ? `${width}px` : width; } else { @@ -213,8 +215,8 @@ const Drawer = { const handler = getComponent(this, 'handle') || false; const getPrefixCls = this.configProvider.getPrefixCls; const prefixCls = getPrefixCls('drawer', customizePrefixCls); - const { class: className } = this.$attrs; - const vcDrawerProps = { + const { class: className } = this.$attrs as any; + const vcDrawerProps: any = { ...this.$attrs, ...omit(rest, [ 'closable', @@ -249,10 +251,10 @@ const Drawer = { }; return {this.renderBody(prefixCls)}; }, -}; +}); /* istanbul ignore next */ -Drawer.install = function(app) { +Drawer.install = function(app: App) { app.component(Drawer.name, Drawer); return app; }; diff --git a/components/drawer/style/index.js b/components/drawer/style/index.ts similarity index 100% rename from components/drawer/style/index.js rename to components/drawer/style/index.ts diff --git a/components/dropdown/dropdown-button.jsx b/components/dropdown/dropdown-button.tsx similarity index 87% rename from components/dropdown/dropdown-button.jsx rename to components/dropdown/dropdown-button.tsx index c4e763f21..ae00867a4 100644 --- a/components/dropdown/dropdown-button.jsx +++ b/components/dropdown/dropdown-button.tsx @@ -1,4 +1,4 @@ -import { provide, inject } from 'vue'; +import { provide, inject, defineComponent } from 'vue'; import Button from '../button'; import classNames from '../_util/classNames'; import buttonTypes from '../button/buttonTypes'; @@ -9,6 +9,7 @@ import { hasProp, getComponent, getSlot } from '../_util/props-util'; import getDropdownProps from './getDropdownProps'; import { defaultConfigProvider } from '../config-provider'; import EllipsisOutlined from '@ant-design/icons-vue/EllipsisOutlined'; +import { tuple } from '../_util/type'; const ButtonTypesProps = buttonTypes(); const DropdownProps = getDropdownProps(); @@ -16,8 +17,8 @@ const ButtonGroup = Button.Group; const DropdownButtonProps = { ...ButtonGroupProps, ...DropdownProps, - type: PropTypes.oneOf(['primary', 'ghost', 'dashed', 'danger', 'default']).def('default'), - size: PropTypes.oneOf(['small', 'large', 'default']).def('default'), + type: PropTypes.oneOf(tuple('primary', 'ghost', 'dashed', 'danger', 'default')).def('default'), + size: PropTypes.oneOf(tuple('small', 'large', 'default')).def('default'), htmlType: ButtonTypesProps.htmlType, href: PropTypes.string, disabled: PropTypes.looseBool, @@ -30,20 +31,21 @@ const DropdownButtonProps = { 'onUpdate:visible': PropTypes.func, }; export { DropdownButtonProps }; -export default { +export default defineComponent({ name: 'ADropdownButton', inheritAttrs: false, props: DropdownButtonProps, setup() { return { configProvider: inject('configProvider', defaultConfigProvider), + popupRef: null }; }, created() { provide('savePopupRef', this.savePopupRef); }, methods: { - savePopupRef(ref) { + savePopupRef(ref: any) { this.popupRef = ref; }, handleClick(e) { @@ -72,12 +74,12 @@ export default { href, title, ...restProps - } = { ...this.$props, ...this.$attrs }; + } = { ...this.$props, ...this.$attrs } as any; const icon = getComponent(this, 'icon') || ; const { getPopupContainer: getContextPopupContainer } = this.configProvider; const getPrefixCls = this.configProvider.getPrefixCls; const prefixCls = getPrefixCls('dropdown-button', customizePrefixCls); - const dropdownProps = { + const dropdownProps: any = { align, disabled, trigger: disabled ? [] : trigger, @@ -112,4 +114,4 @@ export default { ); }, -}; +}); diff --git a/components/dropdown/dropdown.jsx b/components/dropdown/dropdown.tsx similarity index 94% rename from components/dropdown/dropdown.jsx rename to components/dropdown/dropdown.tsx index 4c10e1bfe..bdf142b78 100644 --- a/components/dropdown/dropdown.jsx +++ b/components/dropdown/dropdown.tsx @@ -30,13 +30,14 @@ const Dropdown = defineComponent({ setup() { return { configProvider: inject('configProvider', defaultConfigProvider), + popupRef: null, }; }, created() { provide('savePopupRef', this.savePopupRef); }, methods: { - savePopupRef(ref) { + savePopupRef(ref: any) { this.popupRef = ref; }, getTransitionName() { @@ -49,13 +50,13 @@ const Dropdown = defineComponent({ } return 'slide-up'; }, - renderOverlay(prefixCls) { + renderOverlay(prefixCls: string) { const overlay = getComponent(this, 'overlay'); const overlayNode = Array.isArray(overlay) ? overlay[0] : overlay; // menu cannot be selectable in dropdown defaultly // menu should be focusable in dropdown defaultly const overlayProps = overlayNode && getPropsData(overlayNode); - const { selectable = false, focusable = true } = overlayProps || {}; + const { selectable = false, focusable = true } = (overlayProps || {}) as any; const expandIcon = ( @@ -72,7 +73,7 @@ const Dropdown = defineComponent({ : overlay; return fixedModeOverlay; }, - handleVisibleChange(val) { + handleVisibleChange(val: boolean) { this.$emit('update:visible', val); this.$emit('visibleChange', val); }, diff --git a/components/dropdown/getDropdownProps.js b/components/dropdown/getDropdownProps.ts similarity index 60% rename from components/dropdown/getDropdownProps.js rename to components/dropdown/getDropdownProps.ts index a9e880a35..0a42f8c41 100644 --- a/components/dropdown/getDropdownProps.js +++ b/components/dropdown/getDropdownProps.ts @@ -1,6 +1,11 @@ +import { tuple } from '../_util/type'; +import { PropType } from 'vue'; import PropTypes from '../_util/vue-types'; export default () => ({ - trigger: PropTypes.array.def(['hover']), + trigger: { + type: Array as PropType<('click' | 'hover' | 'contextMenu')[]>, + default: () => ['hover'], + }, overlay: PropTypes.any, visible: PropTypes.looseBool, disabled: PropTypes.looseBool, @@ -8,16 +13,11 @@ export default () => ({ getPopupContainer: PropTypes.func, prefixCls: PropTypes.string, transitionName: PropTypes.string, - placement: PropTypes.oneOf([ - 'topLeft', - 'topCenter', - 'topRight', - 'bottomLeft', - 'bottomCenter', - 'bottomRight', - ]), + placement: PropTypes.oneOf( + tuple('topLeft', 'topCenter', 'topRight', 'bottomLeft', 'bottomCenter', 'bottomRight'), + ), overlayClassName: PropTypes.string, - overlayStyle: PropTypes.object, + overlayStyle: PropTypes.style, forceRender: PropTypes.looseBool, mouseEnterDelay: PropTypes.number, mouseLeaveDelay: PropTypes.number, diff --git a/components/dropdown/index.js b/components/dropdown/index.ts similarity index 62% rename from components/dropdown/index.js rename to components/dropdown/index.ts index 3f11014ce..0ff439c54 100644 --- a/components/dropdown/index.js +++ b/components/dropdown/index.ts @@ -1,16 +1,22 @@ +import { App } from 'vue'; import Dropdown from './dropdown'; import DropdownButton from './dropdown-button'; export { DropdownProps } from './dropdown'; export { DropdownButtonProps } from './dropdown-button'; +type Types = typeof Dropdown; +interface DropdownTypes extends Types { + Button: typeof DropdownButton; +} + Dropdown.Button = DropdownButton; /* istanbul ignore next */ -Dropdown.install = function(app) { +Dropdown.install = function(app: App) { app.component(Dropdown.name, Dropdown); app.component(DropdownButton.name, DropdownButton); return app; }; -export default Dropdown; +export default Dropdown as DropdownTypes; diff --git a/components/form/Form.jsx b/components/form/Form.tsx similarity index 75% rename from components/form/Form.jsx rename to components/form/Form.tsx index 4c03ebe37..14306b05a 100755 --- a/components/form/Form.jsx +++ b/components/form/Form.tsx @@ -1,10 +1,9 @@ -import { inject, provide } from 'vue'; +import { defineComponent, inject, provide, PropType, computed } from 'vue'; import PropTypes from '../_util/vue-types'; import classNames from '../_util/classNames'; -import isRegExp from 'lodash-es/isRegExp'; import warning from '../_util/warning'; import FormItem from './FormItem'; -import { initDefaultProps, getSlot } from '../_util/props-util'; +import { getSlot } from '../_util/props-util'; import { defaultConfigProvider } from '../config-provider'; import { getNamePath, containsNamePath } from './utils/valueUtil'; import { defaultValidateMessages } from './utils/messages'; @@ -12,57 +11,65 @@ import { allPromiseFinish } from './utils/asyncUtil'; import { toArray } from './utils/typeUtil'; import isEqual from 'lodash-es/isEqual'; import scrollIntoView from 'scroll-into-view-if-needed'; +import initDefaultProps from '../_util/props-util/initDefaultProps'; +import { tuple, VueNode } from '../_util/type'; +import { ColProps } from '../grid/col'; +import { InternalNamePath, NamePath, ValidateOptions } from './interface'; + +export type ValidationRule = { + /** validation error message */ + message?: VueNode; + /** built-in validation type, available options: https://github.com/yiminghe/async-validator#type */ + type?: string; + /** indicates whether field is required */ + required?: boolean; + /** treat required fields that only contain whitespace as errors */ + whitespace?: boolean; + /** validate the exact length of a field */ + len?: number; + /** validate the min length of a field */ + min?: number; + /** validate the max length of a field */ + max?: number; + /** validate the value from a list of possible values */ + enum?: string | string[]; + /** validate from a regular expression */ + pattern?: RegExp; + /** transform a value before validation */ + transform?: (value: any) => any; + /** custom validate function (Note: callback must be called) */ + validator?: (rule: any, value: any, callback: any, source?: any, options?: any) => any; + + trigger?: string +}; export const FormProps = { - layout: PropTypes.oneOf(['horizontal', 'inline', 'vertical']), - labelCol: PropTypes.object, - wrapperCol: PropTypes.object, + layout: PropTypes.oneOf(tuple('horizontal', 'inline', 'vertical')), + labelCol: {type: Object as PropType}, + wrapperCol: {type: Object as PropType}, colon: PropTypes.looseBool, - labelAlign: PropTypes.oneOf(['left', 'right']), + labelAlign: PropTypes.oneOf(tuple('left', 'right')), prefixCls: PropTypes.string, hideRequiredMark: PropTypes.looseBool, model: PropTypes.object, - rules: PropTypes.object, - validateMessages: PropTypes.any, + rules: {type: Object as PropType}, + validateMessages: PropTypes.object, validateOnRuleChange: PropTypes.looseBool, // 提交失败自动滚动到第一个错误字段 scrollToFirstError: PropTypes.looseBool, onFinish: PropTypes.func, onFinishFailed: PropTypes.func, - name: PropTypes.name, - validateTrigger: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + name: PropTypes.string, + validateTrigger: {type: [String, Array] as PropType} }; -export const ValidationRule = { - /** validation error message */ - message: PropTypes.string, - /** built-in validation type, available options: https://github.com/yiminghe/async-validator#type */ - type: PropTypes.string, - /** indicates whether field is required */ - required: PropTypes.looseBool, - /** treat required fields that only contain whitespace as errors */ - whitespace: PropTypes.looseBool, - /** validate the exact length of a field */ - len: PropTypes.number, - /** validate the min length of a field */ - min: PropTypes.number, - /** validate the max length of a field */ - max: PropTypes.number, - /** validate the value from a list of possible values */ - enum: PropTypes.oneOfType([String, PropTypes.arrayOf(String)]), - /** validate from a regular expression */ - pattern: PropTypes.custom(isRegExp), - /** transform a value before validation */ - transform: PropTypes.func, - /** custom validate function (Note: callback must be called) */ - validator: PropTypes.func, -}; -function isEqualName(name1, name2) { + +function isEqualName(name1: any, name2: any) { return isEqual(toArray(name1), toArray(name2)); } -const Form = { +const Form = defineComponent({ name: 'AForm', inheritAttrs: false, props: initDefaultProps(FormProps, { @@ -72,14 +79,15 @@ const Form = { }), Item: FormItem, created() { - this.fields = []; - this.form = undefined; - this.lastValidatePromise = null; provide('FormContext', this); }, - setup() { + setup(props) { return { configProvider: inject('configProvider', defaultConfigProvider), + fields: [], + form: undefined, + lastValidatePromise: null, + vertical: computed(()=>props.layout === 'vertical') }; }, watch: { @@ -89,23 +97,18 @@ const Form = { } }, }, - computed: { - vertical() { - return this.layout === 'vertical'; - }, - }, methods: { - addField(field) { + addField(field: any) { if (field) { this.fields.push(field); } }, - removeField(field) { + removeField(field: any) { if (field.fieldName) { this.fields.splice(this.fields.indexOf(field), 1); } }, - handleSubmit(e) { + handleSubmit(e: Event) { e.preventDefault(); e.stopPropagation(); this.$emit('submit', e); @@ -129,7 +132,7 @@ const Form = { ); } }, - resetFields(name) { + resetFields(name: NamePath) { if (!this.model) { warning(false, 'Form', 'model is required for resetFields to work.'); return; @@ -150,10 +153,10 @@ const Form = { this.scrollToField(errorInfo.errorFields[0].name); } }, - validate() { - return this.validateField(...arguments); + validate(...args: any[]) { + return this.validateField(...args); }, - scrollToField(name, options = {}) { + scrollToField(name: string | number , options = {}) { const fields = this.getFieldsByNameList([name]); if (fields.length) { const fieldId = fields[0].fieldId; @@ -169,20 +172,20 @@ const Form = { } }, // eslint-disable-next-line no-unused-vars - getFieldsValue(nameList = true) { - const values = {}; + getFieldsValue(nameList: NamePath[] | true = true) { + const values: any = {}; this.fields.forEach(({ fieldName, fieldValue }) => { values[fieldName] = fieldValue; }); if (nameList === true) { return values; } else { - const res = {}; - toArray(nameList).forEach(namePath => (res[namePath] = values[namePath])); + const res: any = {}; + toArray(nameList as NamePath[]).forEach((namePath) => (res[namePath as string] = values[namePath as string])); return res; } }, - validateFields(nameList, options) { + validateFields(nameList?: NamePath[], options?: ValidateOptions) { warning( !(nameList instanceof Function), 'Form', @@ -193,10 +196,13 @@ const Form = { return Promise.reject('Form `model` is required for validateFields to work.'); } const provideNameList = !!nameList; - const namePathList = provideNameList ? toArray(nameList).map(getNamePath) : []; + const namePathList: InternalNamePath[] = provideNameList ? toArray(nameList).map(getNamePath) : []; // Collect result in promise list - const promiseList = []; + const promiseList: Promise<{ + name: InternalNamePath; + errors: string[]; + }>[] = []; this.fields.forEach(field => { // Add field if not provide `nameList` @@ -225,7 +231,7 @@ const Form = { promiseList.push( promise .then(() => ({ name: fieldNamePath, errors: [] })) - .catch(errors => + .catch((errors: any) => Promise.reject({ name: fieldNamePath, errors, @@ -259,8 +265,8 @@ const Form = { return returnPromise; }, - validateField() { - return this.validateFields(...arguments); + validateField(...args: any[]) { + return this.validateFields(...args); }, }, @@ -282,6 +288,8 @@ const Form = { ); }, -}; +}); -export default Form; +export default Form as typeof Form & { + readonly Item: typeof FormItem +}; diff --git a/components/form/FormItem.jsx b/components/form/FormItem.tsx similarity index 78% rename from components/form/FormItem.jsx rename to components/form/FormItem.tsx index 960359ad2..066dabaae 100644 --- a/components/form/FormItem.jsx +++ b/components/form/FormItem.tsx @@ -1,4 +1,4 @@ -import { inject, provide, Transition } from 'vue'; +import { inject, provide, Transition, PropType, defineComponent, computed } from 'vue'; import cloneDeep from 'lodash-es/cloneDeep'; import PropTypes from '../_util/vue-types'; import classNames from '../_util/classNames'; @@ -6,7 +6,6 @@ import getTransitionProps from '../_util/getTransitionProps'; import Row from '../grid/Row'; import Col from '../grid/Col'; import hasProp, { - initDefaultProps, findDOMNode, getComponent, getOptionProps, @@ -26,6 +25,9 @@ import { getNamePath } from './utils/valueUtil'; import { toArray } from './utils/typeUtil'; import { warning } from '../vc-util/warning'; import find from 'lodash-es/find'; +import { ColProps } from '../grid/col'; +import { tuple, VueNode } from '../_util/type'; +import { ValidateOptions } from './interface'; const iconMap = { success: CheckCircleFilled, @@ -34,7 +36,7 @@ const iconMap = { validating: LoadingOutlined, }; -function getPropByPath(obj, namePathList, strict) { +function getPropByPath(obj: any, namePathList: any, strict?: boolean) { let tempObj = obj; const keyArr = namePathList; @@ -69,38 +71,106 @@ export const FormItemProps = { id: PropTypes.string, htmlFor: PropTypes.string, prefixCls: PropTypes.string, - label: PropTypes.any, - help: PropTypes.any, - extra: PropTypes.any, - labelCol: PropTypes.object, - wrapperCol: PropTypes.object, - hasFeedback: PropTypes.looseBool, + label: PropTypes.VNodeChild, + help: PropTypes.VNodeChild, + extra: PropTypes.VNodeChild, + labelCol: {type: Object as PropType}, + wrapperCol: {type: Object as PropType}, + hasFeedback: PropTypes.looseBool.def(false), colon: PropTypes.looseBool, - labelAlign: PropTypes.oneOf(['left', 'right']), - prop: PropTypes.oneOfType([Array, String, Number]), - name: PropTypes.oneOfType([Array, String, Number]), + labelAlign: PropTypes.oneOf(tuple('left', 'right')), + prop: {type: [String, Number, Array] as PropType}, + name: {type: [String, Number, Array] as PropType}, rules: PropTypes.oneOfType([Array, Object]), - autoLink: PropTypes.looseBool, + autoLink: PropTypes.looseBool.def(true), required: PropTypes.looseBool, validateFirst: PropTypes.looseBool, - validateStatus: PropTypes.oneOf(['', 'success', 'warning', 'error', 'validating']), - validateTrigger: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + validateStatus: PropTypes.oneOf(tuple('', 'success', 'warning', 'error', 'validating')), + validateTrigger: {type: [String, Array] as PropType}, + messageVariables: {type: Object as PropType>}, }; -export default { +export default defineComponent({ name: 'AFormItem', mixins: [BaseMixin], inheritAttrs: false, __ANT_NEW_FORM_ITEM: true, - props: initDefaultProps(FormItemProps, { - hasFeedback: false, - autoLink: true, - }), - setup() { + props: FormItemProps, + setup(props) { + const FormContext = inject('FormContext', {}) as any + const fieldName = computed(()=>props.name || props.prop); + const namePath = computed(()=>{ + const val = fieldName.value + return getNamePath(val) + }) + const fieldId = computed(()=>{ + const {id} = props; + if (id) { + return id; + } else if (!namePath.value.length) { + return undefined; + } else { + const formName = FormContext.name; + const mergedId = namePath.value.join('_'); + return formName ? `${formName}_${mergedId}` : mergedId; + } + }) + const fieldValue = computed(()=> { + const model = FormContext.model; + if (!model || !fieldName.value) { + return; + } + return getPropByPath(model, namePath.value, true).v; + }) + const mergedValidateTrigger = computed(() => { + let validateTrigger = + props.validateTrigger !== undefined + ? props.validateTrigger + : FormContext.validateTrigger; + validateTrigger = validateTrigger === undefined ? 'change' : validateTrigger; + return toArray(validateTrigger); + }); + const getRules = () => { + let formRules = FormContext.rules; + const selfRules = props.rules; + const requiredRule = + props.required !== undefined + ? { required: !!props.required, trigger: mergedValidateTrigger.value } + : []; + const prop = getPropByPath(formRules, namePath.value); + formRules = formRules ? prop.o[prop.k] || prop.v : []; + const rules = [].concat(selfRules || formRules || []); + if (find(rules, rule => rule.required)) { + return rules; + } else { + return rules.concat(requiredRule); + } + }; + const isRequired = computed(() => { + let rules = getRules(); + let isRequired = false; + if (rules && rules.length) { + rules.every(rule => { + if (rule.required) { + isRequired = true; + return false; + } + return true; + }); + } + return isRequired || props.required; + }); return { isFormItemChildren: inject('isFormItemChildren', false), configProvider: inject('configProvider', defaultConfigProvider), - FormContext: inject('FormContext', {}), + FormContext, + fieldId, + fieldName, + namePath, + isRequired, + getRules, + fieldValue, + mergedValidateTrigger }; }, data() { @@ -112,56 +182,9 @@ export default { validator: {}, helpShow: false, errors: [], + initialValue: undefined }; }, - computed: { - fieldName() { - return this.name || this.prop; - }, - namePath() { - return getNamePath(this.fieldName); - }, - fieldId() { - if (this.id) { - return this.id; - } else if (!this.namePath.length) { - return undefined; - } else { - const formName = this.FormContext.name; - const mergedId = this.namePath.join('_'); - return formName ? `${formName}_${mergedId}` : mergedId; - } - }, - fieldValue() { - const model = this.FormContext.model; - if (!model || !this.fieldName) { - return; - } - return getPropByPath(model, this.namePath, true).v; - }, - isRequired() { - let rules = this.getRules(); - let isRequired = false; - if (rules && rules.length) { - rules.every(rule => { - if (rule.required) { - isRequired = true; - return false; - } - return true; - }); - } - return isRequired || this.required; - }, - mergedValidateTrigger() { - let validateTrigger = - this.validateTrigger !== undefined - ? this.validateTrigger - : this.FormContext.validateTrigger; - validateTrigger = validateTrigger === undefined ? 'change' : validateTrigger; - return toArray(validateTrigger); - }, - }, watch: { validateStatus(val) { this.validateState = val; @@ -188,7 +211,7 @@ export default { return fieldName !== undefined ? [...prefixName, ...this.namePath] : []; }, - validateRules(options) { + validateRules(options: ValidateOptions) { const { validateFirst = false, messageVariables } = this.$props; const { triggerName } = options || {}; const namePath = this.getNamePath(); @@ -296,14 +319,14 @@ export default { } }, - onHelpAnimEnd(_key, helpShow) { + onHelpAnimEnd(_key: string, helpShow: boolean) { this.helpShow = helpShow; if (!helpShow) { this.$forceUpdate(); } }, - renderHelp(prefixCls) { + renderHelp(prefixCls: string) { const help = this.getHelpMessage(); const children = help ? (
@@ -324,12 +347,12 @@ export default { ); }, - renderExtra(prefixCls) { + renderExtra(prefixCls: string) { const extra = getComponent(this, 'extra'); return extra ?
{extra}
: null; }, - renderValidateWrapper(prefixCls, c1, c2, c3) { + renderValidateWrapper(prefixCls: string, c1: VueNode, c2: VueNode, c3: VueNode) { const validateStatus = this.validateState; let classes = `${prefixCls}-item-control`; @@ -362,8 +385,8 @@ export default { ); }, - renderWrapper(prefixCls, children) { - const { wrapperCol: contextWrapperCol } = this.isFormItemChildren ? {} : this.FormContext; + renderWrapper(prefixCls: string, children: VueNode) { + const { wrapperCol: contextWrapperCol } = (this.isFormItemChildren ? {} : this.FormContext) as any; const { wrapperCol } = this; const mergedWrapperCol = wrapperCol || contextWrapperCol || {}; const { style, id, ...restProps } = mergedWrapperCol; @@ -378,7 +401,7 @@ export default { return {children}; }, - renderLabel(prefixCls) { + renderLabel(prefixCls: string) { const { vertical, labelAlign: contextLabelAlign, @@ -437,7 +460,7 @@ export default { ) : null; }, - renderChildren(prefixCls, child) { + renderChildren(prefixCls: string, child: VueNode) { return [ this.renderLabel(prefixCls), this.renderWrapper( @@ -451,9 +474,9 @@ export default { ), ]; }, - renderFormItem(child) { + renderFormItem(child: any[]) { const { prefixCls: customizePrefixCls } = this.$props; - const { class: className, ...restProps } = this.$attrs; + const { class: className, ...restProps } = this.$attrs as any; const getPrefixCls = this.configProvider.getPrefixCls; const prefixCls = getPrefixCls('form', customizePrefixCls); const children = this.renderChildren(prefixCls, child); @@ -480,11 +503,11 @@ export default { const originalChange = originalEvents.onChange; firstChildren = cloneElement(firstChildren, { ...(this.fieldId ? { id: this.fieldId } : undefined), - onBlur: (...args) => { + onBlur: (...args: any[]) => { originalBlur && originalBlur(...args); this.onFieldBlur(); }, - onChange: (...args) => { + onChange: (...args: any[]) => { if (Array.isArray(originalChange)) { for (let i = 0, l = originalChange.length; i < l; i++) { originalChange[i](...args); @@ -498,4 +521,4 @@ export default { } return this.renderFormItem([firstChildren, children.slice(1)]); }, -}; +}); diff --git a/components/form/index.jsx b/components/form/index.jsx index a232328d1..b0af7de54 100644 --- a/components/form/index.jsx +++ b/components/form/index.jsx @@ -1,7 +1,6 @@ import Form from './Form'; -export { FormProps, ValidationRule } from './Form'; -export { FormItemProps } from './FormItem'; +export { FormProps } from './Form'; /* istanbul ignore next */ Form.install = function(app) { diff --git a/components/form/interface.ts b/components/form/interface.ts new file mode 100644 index 000000000..76c35da82 --- /dev/null +++ b/components/form/interface.ts @@ -0,0 +1,201 @@ +import { VueNode } from '../_util/type'; + +export type InternalNamePath = (string | number)[]; +export type NamePath = string | number | InternalNamePath; + +export type StoreValue = any; +export interface Store { + [name: string]: StoreValue; +} + +export interface Meta { + touched: boolean; + validating: boolean; + errors: string[]; + name: InternalNamePath; +} + +export interface InternalFieldData extends Meta { + value: StoreValue; +} + +/** + * Used by `setFields` config + */ +export interface FieldData extends Partial> { + name: NamePath; +} + +export type RuleType = + | 'string' + | 'number' + | 'boolean' + | 'method' + | 'regexp' + | 'integer' + | 'float' + | 'object' + | 'enum' + | 'date' + | 'url' + | 'hex' + | 'email'; + +type Validator = ( + rule: RuleObject, + value: StoreValue, + callback: (error?: string) => void, +) => Promise | void; + +export interface ValidatorRule { + message?: string | VueNode; + validator: Validator; +} + +interface BaseRule { + enum?: StoreValue[]; + len?: number; + max?: number; + message?: string | VueNode; + min?: number; + pattern?: RegExp; + required?: boolean; + transform?: (value: StoreValue) => StoreValue; + type?: RuleType; + whitespace?: boolean; + + /** Customize rule level `validateTrigger`. Must be subset of Field `validateTrigger` */ + validateTrigger?: string | string[]; +} + +type AggregationRule = BaseRule & Partial; + +interface ArrayRule extends Omit { + type: 'array'; + defaultField?: RuleObject; +} + +export type RuleObject = AggregationRule | ArrayRule; + +export type Rule = RuleObject; + +export interface ValidateErrorEntity { + values: Values; + errorFields: { name: InternalNamePath; errors: string[] }[]; + outOfDate: boolean; +} + +export interface FieldError { + name: InternalNamePath; + errors: string[]; +} + +export interface ValidateOptions { + triggerName?: string; + validateMessages?: ValidateMessages; +} + +export type InternalValidateFields = ( + nameList?: NamePath[], + options?: ValidateOptions, +) => Promise; +export type ValidateFields = (nameList?: NamePath[]) => Promise; + +// >>>>>> Info +interface ValueUpdateInfo { + type: 'valueUpdate'; + source: 'internal' | 'external'; +} + +interface ValidateFinishInfo { + type: 'validateFinish'; +} + +interface ResetInfo { + type: 'reset'; +} + +interface SetFieldInfo { + type: 'setField'; + data: FieldData; +} + +interface DependenciesUpdateInfo { + type: 'dependenciesUpdate'; + /** + * Contains all the related `InternalNamePath[]`. + * a <- b <- c : change `a` + * relatedFields=[a, b, c] + */ + relatedFields: InternalNamePath[]; +} + +export type NotifyInfo = + | ValueUpdateInfo + | ValidateFinishInfo + | ResetInfo + | SetFieldInfo + | DependenciesUpdateInfo; + +export type ValuedNotifyInfo = NotifyInfo & { + store: Store; +}; + +export interface Callbacks { + onValuesChange?: (changedValues: any, values: Values) => void; + onFieldsChange?: (changedFields: FieldData[], allFields: FieldData[]) => void; + onFinish?: (values: Values) => void; + onFinishFailed?: (errorInfo: ValidateErrorEntity) => void; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type EventArgs = any[]; + +type ValidateMessage = string | (() => string); +export interface ValidateMessages { + default?: ValidateMessage; + required?: ValidateMessage; + enum?: ValidateMessage; + whitespace?: ValidateMessage; + date?: { + format?: ValidateMessage; + parse?: ValidateMessage; + invalid?: ValidateMessage; + }; + types?: { + string?: ValidateMessage; + method?: ValidateMessage; + array?: ValidateMessage; + object?: ValidateMessage; + number?: ValidateMessage; + date?: ValidateMessage; + boolean?: ValidateMessage; + integer?: ValidateMessage; + float?: ValidateMessage; + regexp?: ValidateMessage; + email?: ValidateMessage; + url?: ValidateMessage; + hex?: ValidateMessage; + }; + string?: { + len?: ValidateMessage; + min?: ValidateMessage; + max?: ValidateMessage; + range?: ValidateMessage; + }; + number?: { + len?: ValidateMessage; + min?: ValidateMessage; + max?: ValidateMessage; + range?: ValidateMessage; + }; + array?: { + len?: ValidateMessage; + min?: ValidateMessage; + max?: ValidateMessage; + range?: ValidateMessage; + }; + pattern?: { + mismatch?: ValidateMessage; + }; +} diff --git a/components/form/utils/asyncUtil.js b/components/form/utils/asyncUtil.ts similarity index 81% rename from components/form/utils/asyncUtil.js rename to components/form/utils/asyncUtil.ts index 3c5b0df6c..217de740e 100644 --- a/components/form/utils/asyncUtil.js +++ b/components/form/utils/asyncUtil.ts @@ -1,4 +1,5 @@ -export function allPromiseFinish(promiseList) { +import { FieldError } from '../interface'; +export function allPromiseFinish(promiseList: Promise[]): Promise { let hasError = false; let count = promiseList.length; const results = []; diff --git a/components/form/utils/messages.js b/components/form/utils/messages.ts similarity index 100% rename from components/form/utils/messages.js rename to components/form/utils/messages.ts diff --git a/components/form/utils/typeUtil.js b/components/form/utils/typeUtil.ts similarity index 67% rename from components/form/utils/typeUtil.js rename to components/form/utils/typeUtil.ts index e62954c28..bb3195719 100644 --- a/components/form/utils/typeUtil.js +++ b/components/form/utils/typeUtil.ts @@ -1,4 +1,4 @@ -export function toArray(value) { +export function toArray(value?: T | T[] | null): T[] { if (value === undefined || value === null) { return []; } diff --git a/components/form/utils/validateUtil.js b/components/form/utils/validateUtil.ts similarity index 73% rename from components/form/utils/validateUtil.js rename to components/form/utils/validateUtil.ts index b75907935..7cd9c5d4d 100644 --- a/components/form/utils/validateUtil.js +++ b/components/form/utils/validateUtil.ts @@ -5,15 +5,16 @@ import { warning } from '../../vc-util/warning'; import { setValues } from './valueUtil'; import { defaultValidateMessages } from './messages'; import { isValidElement } from '../../_util/props-util'; +import { InternalNamePath, RuleObject, ValidateMessages, ValidateOptions } from '../interface'; // Remove incorrect original ts define -const AsyncValidator = RawAsyncValidator; +const AsyncValidator: any = RawAsyncValidator; /** * Replace with template. * `I'm ${name}` + { name: 'bamboo' } = I'm bamboo */ -function replaceMessage(template, kv) { +function replaceMessage(template: string, kv: Record): string { return template.replace(/\$\{\w+\}/g, str => { const key = str.slice(2, -1); return kv[key]; @@ -24,18 +25,23 @@ function replaceMessage(template, kv) { * We use `async-validator` to validate rules. So have to hot replace the message with validator. * { required: '${name} is required' } => { required: () => 'field is required' } */ -function convertMessages(messages, name, rule, messageVariables) { +function convertMessages( + messages: ValidateMessages, + name: string, + rule: RuleObject, + messageVariables?: Record, +): ValidateMessages { const kv = { - ...rule, + ...(rule as Record), name, enum: (rule.enum || []).join(', '), }; - const replaceFunc = (template, additionalKV) => () => + const replaceFunc = (template: string, additionalKV?: Record) => () => replaceMessage(template, { ...kv, ...additionalKV }); /* eslint-disable no-param-reassign */ - function fillTemplate(source, target = {}) { + function fillTemplate(source: ValidateMessages, target: ValidateMessages = {}) { Object.keys(source).forEach(ruleName => { const value = source[ruleName]; if (typeof value === 'string') { @@ -52,13 +58,19 @@ function convertMessages(messages, name, rule, messageVariables) { } /* eslint-enable */ - return fillTemplate(setValues({}, defaultValidateMessages, messages)); + return fillTemplate(setValues({}, defaultValidateMessages, messages)) as ValidateMessages; } -async function validateRule(name, value, rule, options, messageVariables) { +async function validateRule( + name: string, + value: any, + rule: RuleObject, + options: ValidateOptions, + messageVariables?: Record, +): Promise { const cloneRule = { ...rule }; // We should special handle array validate - let subRuleField = null; + let subRuleField: RuleObject = null; if (cloneRule && cloneRule.type === 'array' && cloneRule.defaultField) { subRuleField = cloneRule.defaultField; delete cloneRule.defaultField; @@ -77,19 +89,19 @@ async function validateRule(name, value, rule, options, messageVariables) { await Promise.resolve(validator.validate({ [name]: value }, { ...options })); } catch (errObj) { if (errObj.errors) { - result = errObj.errors.map(({ message }, index) => + result = errObj.errors.map(({ message }, index: number) => // Wrap VueNode with `key` isValidElement(message) ? cloneVNode(message, { key: `error_${index}` }) : message, ); } else { console.error(errObj); - result = [messages.default()]; + result = [(messages.default as () => string)()]; } } if (!result.length && subRuleField) { - const subResults = await Promise.all( - value.map((subValue, i) => + const subResults: string[][] = await Promise.all( + (value as any[]).map((subValue: any, i: number) => validateRule(`${name}.${i}`, subValue, subRuleField, options, messageVariables), ), ); @@ -104,11 +116,18 @@ async function validateRule(name, value, rule, options, messageVariables) { * We use `async-validator` to validate the value. * But only check one value in a time to avoid namePath validate issue. */ -export function validateRules(namePath, value, rules, options, validateFirst, messageVariables) { +export function validateRules( + namePath: InternalNamePath, + value: any, + rules: RuleObject[], + options: ValidateOptions, + validateFirst: boolean | 'parallel', + messageVariables?: Record, +) { const name = namePath.join('.'); // Fill rule with context - const filledRules = rules.map(currentRule => { + const filledRules: RuleObject[] = rules.map(currentRule => { const originValidatorFunc = currentRule.validator; if (!originValidatorFunc) { @@ -116,11 +135,11 @@ export function validateRules(namePath, value, rules, options, validateFirst, me } return { ...currentRule, - validator(rule, val, callback) { + validator(rule: RuleObject, val: any, callback: (error?: string) => void) { let hasPromise = false; // Wrap callback only accept when promise not provided - const wrappedCallback = (...args) => { + const wrappedCallback = (...args: string[]) => { // Wait a tick to make sure return type is a promise Promise.resolve().then(() => { warning( @@ -146,7 +165,7 @@ export function validateRules(namePath, value, rules, options, validateFirst, me warning(hasPromise, '`callback` is deprecated. Please return a promise instead.'); if (hasPromise) { - promise + (promise as Promise) .then(() => { callback(); }) @@ -158,7 +177,7 @@ export function validateRules(namePath, value, rules, options, validateFirst, me }; }); - let summaryPromise; + let summaryPromise: Promise; if (validateFirst === true) { // >>>>> Validate by serialization @@ -184,12 +203,12 @@ export function validateRules(namePath, value, rules, options, validateFirst, me summaryPromise = (validateFirst ? finishOnFirstFailed(rulePromises) : finishOnAllFailed(rulePromises) - ).then(errors => { + ).then((errors: string[]): string[] | Promise => { if (!errors.length) { return []; } - return Promise.reject(errors); + return Promise.reject(errors); }); } @@ -199,7 +218,7 @@ export function validateRules(namePath, value, rules, options, validateFirst, me return summaryPromise; } -async function finishOnAllFailed(rulePromises) { +async function finishOnAllFailed(rulePromises: Promise[]): Promise { return Promise.all(rulePromises).then(errorsList => { const errors = [].concat(...errorsList); @@ -207,7 +226,7 @@ async function finishOnAllFailed(rulePromises) { }); } -async function finishOnFirstFailed(rulePromises) { +async function finishOnFirstFailed(rulePromises: Promise[]): Promise { let count = 0; return new Promise(resolve => { diff --git a/components/form/utils/valueUtil.js b/components/form/utils/valueUtil.ts similarity index 53% rename from components/form/utils/valueUtil.js rename to components/form/utils/valueUtil.ts index 1f1733146..0a921252d 100644 --- a/components/form/utils/valueUtil.js +++ b/components/form/utils/valueUtil.ts @@ -1,4 +1,5 @@ import { toArray } from './typeUtil'; +import { InternalNamePath, NamePath } from '../interface'; /** * Convert name to internal supported format. @@ -7,35 +8,15 @@ import { toArray } from './typeUtil'; * 123 => [123] * ['a', 123] => ['a', 123] */ -export function getNamePath(path) { +export function getNamePath(path: NamePath | null): InternalNamePath { return toArray(path); } -// export function getValue(store: Store, namePath: InternalNamePath) { -// const value = get(store, namePath); -// return value; -// } - -// export function setValue(store: Store, namePath: InternalNamePath, value: StoreValue): Store { -// const newStore = set(store, namePath, value); -// return newStore; -// } - -// export function cloneByNamePathList(store: Store, namePathList: InternalNamePath[]): Store { -// let newStore = {}; -// namePathList.forEach(namePath => { -// const value = getValue(store, namePath); -// newStore = setValue(newStore, namePath, value); -// }); - -// return newStore; -// } - -export function containsNamePath(namePathList, namePath) { +export function containsNamePath(namePathList: InternalNamePath[], namePath: InternalNamePath) { return namePathList && namePathList.some(path => matchNamePath(path, namePath)); } -function isObject(obj) { +function isObject(obj: any) { return typeof obj === 'object' && obj !== null && Object.getPrototypeOf(obj) === Object.prototype; } @@ -43,8 +24,8 @@ function isObject(obj) { * Copy values into store and return a new values object * ({ a: 1, b: { c: 2 } }, { a: 4, b: { d: 5 } }) => { a: 4, b: { c: 2, d: 5 } } */ -function internalSetValues(store, values) { - const newStore = Array.isArray(store) ? [...store] : { ...store }; +function internalSetValues(store: T, values: T): T { + const newStore: T = (Array.isArray(store) ? [...store] : { ...store }) as T; if (!values) { return newStore; @@ -62,11 +43,17 @@ function internalSetValues(store, values) { return newStore; } -export function setValues(store, ...restValues) { - return restValues.reduce((current, newStore) => internalSetValues(current, newStore), store); +export function setValues(store: T, ...restValues: T[]): T { + return restValues.reduce( + (current: T, newStore: T) => internalSetValues(current, newStore), + store, + ); } -export function matchNamePath(namePath, changedNamePath) { +export function matchNamePath( + namePath: InternalNamePath, + changedNamePath: InternalNamePath | null, +) { if (!namePath || !changedNamePath || namePath.length !== changedNamePath.length) { return false; }