]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(types): deny unknown attributes on component by default (#1614)
authorHcySunYang <HcySunYang@outlook.com>
Fri, 17 Jul 2020 15:43:28 +0000 (23:43 +0800)
committerGitHub <noreply@github.com>
Fri, 17 Jul 2020 15:43:28 +0000 (11:43 -0400)
close #1519

packages/runtime-core/src/apiDefineComponent.ts
packages/runtime-core/src/components/Suspense.ts
packages/runtime-core/src/components/Teleport.ts
packages/runtime-core/src/h.ts
packages/runtime-core/src/hydration.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/vnode.ts
test-dts/componentTypeExtensions.test-d.ts [deleted file]
test-dts/componentTypeExtensions.test-d.tsx [new file with mode: 0644]
test-dts/defineComponent.test-d.tsx

index 6561c1c76067f5fd57df78cfd44410b714ae0010..ea53f96ad1190a3c755e02286d184db79a8b1bf5 100644 (file)
@@ -15,7 +15,11 @@ import {
 import { ExtractPropTypes, ComponentPropsOptions } from './componentProps'
 import { EmitsOptions } from './componentEmits'
 import { isFunction } from '@vue/shared'
-import { VNodeProps } from './vnode'
+import {
+  VNodeProps,
+  AllowedComponentProps,
+  ComponentCustomProps
+} from './vnode'
 
 // defineComponent is a utility that is primarily used for type inference
 // when declaring components. Type inference is provided in the component
@@ -40,7 +44,7 @@ export function defineComponent<Props, RawBindings = object>(
     {},
     {},
     // public props
-    VNodeProps & Props
+    VNodeProps & Props & AllowedComponentProps & ComponentCustomProps
   >
 > &
   FunctionalComponent<Props>
@@ -80,7 +84,7 @@ export function defineComponent<
     Mixin,
     Extends,
     E,
-    VNodeProps & Props
+    VNodeProps & Props & AllowedComponentProps & ComponentCustomProps
   >
 > &
   ComponentOptionsWithoutProps<
@@ -131,7 +135,8 @@ export function defineComponent<
     M,
     Mixin,
     Extends,
-    E
+    E,
+    AllowedComponentProps & ComponentCustomProps
   >
 > &
   ComponentOptionsWithArrayProps<
@@ -182,7 +187,7 @@ export function defineComponent<
     Mixin,
     Extends,
     E,
-    VNodeProps
+    VNodeProps & AllowedComponentProps & ComponentCustomProps
   >
 > &
   ComponentOptionsWithObjectProps<
index be725ffced79931e6581606162bf5d93ad42a1ef..89ed331b0b7f72db410acde9241d9ab5a53c6fd0 100644 (file)
@@ -197,7 +197,7 @@ function patchSuspense(
 }
 
 export interface SuspenseBoundary {
-  vnode: VNode
+  vnode: VNode<RendererNode, RendererElement, SuspenseProps>
   parent: SuspenseBoundary | null
   parentComponent: ComponentInternalInstance | null
   isSVG: boolean
index 9d8034684df13e329fb5937421f90e86cd8b9035..ceeb451df0ad6329054e7709eb399cb02130a8eb 100644 (file)
@@ -11,6 +11,8 @@ import { VNode, VNodeArrayChildren, VNodeProps } from '../vnode'
 import { isString, ShapeFlags } from '@vue/shared'
 import { warn } from '../warning'
 
+export type TeleportVNode = VNode<RendererNode, RendererElement, TeleportProps>
+
 export interface TeleportProps {
   to: string | RendererElement
   disabled?: boolean
@@ -55,8 +57,8 @@ const resolveTarget = <T = RendererElement>(
 export const TeleportImpl = {
   __isTeleport: true,
   process(
-    n1: VNode | null,
-    n2: VNode,
+    n1: TeleportVNode | null,
+    n2: TeleportVNode,
     container: RendererElement,
     anchor: RendererNode | null,
     parentComponent: ComponentInternalInstance | null,
@@ -85,10 +87,7 @@ export const TeleportImpl = {
       insert(placeholder, container, anchor)
       insert(mainAnchor, container, anchor)
 
-      const target = (n2.target = resolveTarget(
-        n2.props as TeleportProps,
-        querySelector
-      ))
+      const target = (n2.target = resolveTarget(n2.props, querySelector))
       const targetAnchor = (n2.targetAnchor = createText(''))
       if (target) {
         insert(targetAnchor, target)
@@ -165,7 +164,7 @@ export const TeleportImpl = {
         // target changed
         if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) {
           const nextTarget = (n2.target = resolveTarget(
-            n2.props as TeleportProps,
+            n2.props,
             querySelector
           ))
           if (nextTarget) {
@@ -267,7 +266,7 @@ interface TeleportTargetElement extends Element {
 
 function hydrateTeleport(
   node: Node,
-  vnode: VNode,
+  vnode: TeleportVNode,
   parentComponent: ComponentInternalInstance | null,
   parentSuspense: SuspenseBoundary | null,
   optimized: boolean,
@@ -284,7 +283,7 @@ function hydrateTeleport(
   ) => Node | null
 ): Node | null {
   const target = (vnode.target = resolveTarget<Element>(
-    vnode.props as TeleportProps,
+    vnode.props,
     querySelector
   ))
   if (target) {
index a00e465bdf799f6320a73491d7fdff68b5be0770..42d9c9714384a6878816cd2577b48149687cea59 100644 (file)
@@ -50,7 +50,7 @@ type RawProps = VNodeProps & {
   __v_isVNode?: never
   // used to differ from Array children
   [Symbol.iterator]?: never
-}
+} & { [key: string]: any }
 
 type RawChildren =
   | string
index 81b1587988739b3f8d4b81346325e1e634bad65f..aa92891d4b2438b82f37187f58f8b398e49135bd 100644 (file)
@@ -18,7 +18,7 @@ import {
   SuspenseBoundary,
   queueEffectWithSuspense
 } from './components/Suspense'
-import { TeleportImpl } from './components/Teleport'
+import { TeleportImpl, TeleportVNode } from './components/Teleport'
 
 export type RootHydrateFunction = (
   vnode: VNode<Node, Element>,
@@ -202,7 +202,7 @@ export function createHydrationFunctions(
           } else {
             nextNode = (vnode.type as typeof TeleportImpl).hydrate(
               node,
-              vnode,
+              vnode as TeleportVNode,
               parentComponent,
               parentSuspense,
               optimized,
index 4ab1b396cb457f702a07bfaef36e4c06a7e0a307..39de3cca2a3776ff565290a1cc30bbb1c9bb155a 100644 (file)
@@ -54,7 +54,7 @@ export { h } from './h'
 // Advanced render function utilities
 export { createVNode, cloneVNode, mergeProps, isVNode } from './vnode'
 // VNode types
-export { Fragment, Text, Comment, Static } from './vnode'
+export { Fragment, Text, Comment, Static, ComponentCustomProps } from './vnode'
 // Built-in components
 export { Teleport, TeleportProps } from './components/Teleport'
 export { Suspense, SuspenseProps } from './components/Suspense'
index d160bdd078ce3cd1f1f866418a145e922dc6e491..7f74d4343267931638da789dc03b50b9e4204aae 100644 (file)
@@ -53,7 +53,7 @@ import {
   queueEffectWithSuspense,
   SuspenseImpl
 } from './components/Suspense'
-import { TeleportImpl } from './components/Teleport'
+import { TeleportImpl, TeleportVNode } from './components/Teleport'
 import { isKeepAlive, KeepAliveContext } from './components/KeepAlive'
 import { registerHMR, unregisterHMR, isHmrUpdating } from './hmr'
 import {
@@ -477,8 +477,8 @@ function baseCreateRenderer(
           )
         } else if (shapeFlag & ShapeFlags.TELEPORT) {
           ;(type as typeof TeleportImpl).process(
-            n1,
-            n2,
+            n1 as TeleportVNode,
+            n2 as TeleportVNode,
             container,
             anchor,
             parentComponent,
index a96519cb94c998bf83c3680e5989980a16402f2d..30b5c240ad4fb7eb29179ce351769e3405f89ee3 100644 (file)
@@ -71,8 +71,14 @@ export type VNodeHook =
   | VNodeMountHook[]
   | VNodeUpdateHook[]
 
-export interface VNodeProps {
-  [key: string]: any
+export interface ComponentCustomProps {}
+export interface AllowedComponentProps {
+  class?: unknown
+  style?: unknown
+}
+
+// https://github.com/microsoft/TypeScript/issues/33099
+export type VNodeProps = {
   key?: string | number
   ref?: VNodeRef
 
@@ -104,7 +110,11 @@ export type VNodeNormalizedChildren =
   | RawSlots
   | null
 
-export interface VNode<HostNode = RendererNode, HostElement = RendererElement> {
+export interface VNode<
+  HostNode = RendererNode,
+  HostElement = RendererElement,
+  ExtraProps = { [key: string]: any }
+> {
   /**
    * @internal
    */
@@ -114,7 +124,7 @@ export interface VNode<HostNode = RendererNode, HostElement = RendererElement> {
    */
   __v_skip: true
   type: VNodeTypes
-  props: VNodeProps | null
+  props: (VNodeProps & ExtraProps) | null
   key: string | number | null
   ref: VNodeNormalizedRef | null
   scopeId: string | null // SFC only
@@ -597,7 +607,7 @@ export function mergeProps(...args: (Data & VNodeProps)[]) {
         const incoming = toMerge[key]
         if (existing !== incoming) {
           ret[key] = existing
-            ? [].concat(existing as any, toMerge[key])
+            ? [].concat(existing as any, toMerge[key] as any)
             : incoming
         }
       } else {
diff --git a/test-dts/componentTypeExtensions.test-d.ts b/test-dts/componentTypeExtensions.test-d.ts
deleted file mode 100644 (file)
index 1d543ac..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-import { defineComponent, expectError, expectType } from './index'
-
-declare module '@vue/runtime-core' {
-  interface ComponentCustomOptions {
-    test?(n: number): void
-  }
-
-  interface ComponentCustomProperties {
-    state: 'stopped' | 'running'
-  }
-}
-
-export const Custom = defineComponent({
-  data: () => ({ counter: 0 }),
-
-  test(n) {
-    expectType<number>(n)
-  },
-
-  methods: {
-    aMethod() {
-      // @ts-expect-error
-      expectError(this.notExisting)
-      this.counter++
-      this.state = 'running'
-      // @ts-expect-error
-      expectError((this.state = 'not valid'))
-    }
-  }
-})
diff --git a/test-dts/componentTypeExtensions.test-d.tsx b/test-dts/componentTypeExtensions.test-d.tsx
new file mode 100644 (file)
index 0000000..32a72f8
--- /dev/null
@@ -0,0 +1,57 @@
+import { defineComponent, expectError, expectType } from './index'
+
+declare module '@vue/runtime-core' {
+  interface ComponentCustomOptions {
+    test?(n: number): void
+  }
+
+  interface ComponentCustomProperties {
+    state: 'stopped' | 'running'
+  }
+
+  interface ComponentCustomProps {
+    custom?: number
+  }
+}
+
+export const Custom = defineComponent({
+  props: {
+    bar: String,
+    baz: {
+      type: Number,
+      required: true
+    }
+  },
+
+  data: () => ({ counter: 0 }),
+
+  test(n) {
+    expectType<number>(n)
+  },
+
+  methods: {
+    aMethod() {
+      // @ts-expect-error
+      expectError(this.notExisting)
+      this.counter++
+      this.state = 'running'
+      // @ts-expect-error
+      expectError((this.state = 'not valid'))
+    }
+  }
+})
+
+expectType<JSX.Element>(<Custom baz={1} />)
+expectType<JSX.Element>(<Custom custom={1} baz={1} />)
+expectType<JSX.Element>(<Custom bar="bar" baz={1} />)
+
+// @ts-expect-error
+expectType<JSX.Element>(<Custom />)
+// @ts-expect-error
+expectError(<Custom bar="bar" />)
+// @ts-expect-error
+expectError(<Custom baz="baz" />)
+// @ts-expect-error
+expectError(<Custom baz={1} notExist={1} />)
+// @ts-expect-error
+expectError(<Custom baz={1} custom="custom" />)
index 6bfd968a7f99afc381fdd9e469cc89357d7badb4..f37c0eb1eaa9d6ac8f058c36d56942ba8d0fdab1 100644 (file)
@@ -171,8 +171,9 @@ describe('with object props', () => {
       eee={() => ({ a: 'eee' })}
       fff={(a, b) => ({ a: a > +b })}
       hhh={false}
-      // should allow extraneous as attrs
+      // should allow class/style as attrs
       class="bar"
+      style={{ color: 'red' }}
       // should allow key
       key={'foo'}
       // should allow ref