feat(types): deny unknown attributes on component by default (#1614)

close #1519
This commit is contained in:
HcySunYang 2020-07-17 23:43:28 +08:00 committed by GitHub
parent 77659fa037
commit 5d8a64d53a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 100 additions and 58 deletions

View File

@ -15,7 +15,11 @@ import {
import { ExtractPropTypes, ComponentPropsOptions } from './componentProps' import { ExtractPropTypes, ComponentPropsOptions } from './componentProps'
import { EmitsOptions } from './componentEmits' import { EmitsOptions } from './componentEmits'
import { isFunction } from '@vue/shared' 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 // defineComponent is a utility that is primarily used for type inference
// when declaring components. Type inference is provided in the component // when declaring components. Type inference is provided in the component
@ -40,7 +44,7 @@ export function defineComponent<Props, RawBindings = object>(
{}, {},
{}, {},
// public props // public props
VNodeProps & Props VNodeProps & Props & AllowedComponentProps & ComponentCustomProps
> >
> & > &
FunctionalComponent<Props> FunctionalComponent<Props>
@ -80,7 +84,7 @@ export function defineComponent<
Mixin, Mixin,
Extends, Extends,
E, E,
VNodeProps & Props VNodeProps & Props & AllowedComponentProps & ComponentCustomProps
> >
> & > &
ComponentOptionsWithoutProps< ComponentOptionsWithoutProps<
@ -131,7 +135,8 @@ export function defineComponent<
M, M,
Mixin, Mixin,
Extends, Extends,
E E,
AllowedComponentProps & ComponentCustomProps
> >
> & > &
ComponentOptionsWithArrayProps< ComponentOptionsWithArrayProps<
@ -182,7 +187,7 @@ export function defineComponent<
Mixin, Mixin,
Extends, Extends,
E, E,
VNodeProps VNodeProps & AllowedComponentProps & ComponentCustomProps
> >
> & > &
ComponentOptionsWithObjectProps< ComponentOptionsWithObjectProps<

View File

@ -197,7 +197,7 @@ function patchSuspense(
} }
export interface SuspenseBoundary { export interface SuspenseBoundary {
vnode: VNode vnode: VNode<RendererNode, RendererElement, SuspenseProps>
parent: SuspenseBoundary | null parent: SuspenseBoundary | null
parentComponent: ComponentInternalInstance | null parentComponent: ComponentInternalInstance | null
isSVG: boolean isSVG: boolean

View File

@ -11,6 +11,8 @@ import { VNode, VNodeArrayChildren, VNodeProps } from '../vnode'
import { isString, ShapeFlags } from '@vue/shared' import { isString, ShapeFlags } from '@vue/shared'
import { warn } from '../warning' import { warn } from '../warning'
export type TeleportVNode = VNode<RendererNode, RendererElement, TeleportProps>
export interface TeleportProps { export interface TeleportProps {
to: string | RendererElement to: string | RendererElement
disabled?: boolean disabled?: boolean
@ -55,8 +57,8 @@ const resolveTarget = <T = RendererElement>(
export const TeleportImpl = { export const TeleportImpl = {
__isTeleport: true, __isTeleport: true,
process( process(
n1: VNode | null, n1: TeleportVNode | null,
n2: VNode, n2: TeleportVNode,
container: RendererElement, container: RendererElement,
anchor: RendererNode | null, anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
@ -85,10 +87,7 @@ export const TeleportImpl = {
insert(placeholder, container, anchor) insert(placeholder, container, anchor)
insert(mainAnchor, container, anchor) insert(mainAnchor, container, anchor)
const target = (n2.target = resolveTarget( const target = (n2.target = resolveTarget(n2.props, querySelector))
n2.props as TeleportProps,
querySelector
))
const targetAnchor = (n2.targetAnchor = createText('')) const targetAnchor = (n2.targetAnchor = createText(''))
if (target) { if (target) {
insert(targetAnchor, target) insert(targetAnchor, target)
@ -165,7 +164,7 @@ export const TeleportImpl = {
// target changed // target changed
if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) { if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) {
const nextTarget = (n2.target = resolveTarget( const nextTarget = (n2.target = resolveTarget(
n2.props as TeleportProps, n2.props,
querySelector querySelector
)) ))
if (nextTarget) { if (nextTarget) {
@ -267,7 +266,7 @@ interface TeleportTargetElement extends Element {
function hydrateTeleport( function hydrateTeleport(
node: Node, node: Node,
vnode: VNode, vnode: TeleportVNode,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
optimized: boolean, optimized: boolean,
@ -284,7 +283,7 @@ function hydrateTeleport(
) => Node | null ) => Node | null
): Node | null { ): Node | null {
const target = (vnode.target = resolveTarget<Element>( const target = (vnode.target = resolveTarget<Element>(
vnode.props as TeleportProps, vnode.props,
querySelector querySelector
)) ))
if (target) { if (target) {

View File

@ -50,7 +50,7 @@ type RawProps = VNodeProps & {
__v_isVNode?: never __v_isVNode?: never
// used to differ from Array children // used to differ from Array children
[Symbol.iterator]?: never [Symbol.iterator]?: never
} } & { [key: string]: any }
type RawChildren = type RawChildren =
| string | string

View File

@ -18,7 +18,7 @@ import {
SuspenseBoundary, SuspenseBoundary,
queueEffectWithSuspense queueEffectWithSuspense
} from './components/Suspense' } from './components/Suspense'
import { TeleportImpl } from './components/Teleport' import { TeleportImpl, TeleportVNode } from './components/Teleport'
export type RootHydrateFunction = ( export type RootHydrateFunction = (
vnode: VNode<Node, Element>, vnode: VNode<Node, Element>,
@ -202,7 +202,7 @@ export function createHydrationFunctions(
} else { } else {
nextNode = (vnode.type as typeof TeleportImpl).hydrate( nextNode = (vnode.type as typeof TeleportImpl).hydrate(
node, node,
vnode, vnode as TeleportVNode,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
optimized, optimized,

View File

@ -54,7 +54,7 @@ export { h } from './h'
// Advanced render function utilities // Advanced render function utilities
export { createVNode, cloneVNode, mergeProps, isVNode } from './vnode' export { createVNode, cloneVNode, mergeProps, isVNode } from './vnode'
// VNode types // VNode types
export { Fragment, Text, Comment, Static } from './vnode' export { Fragment, Text, Comment, Static, ComponentCustomProps } from './vnode'
// Built-in components // Built-in components
export { Teleport, TeleportProps } from './components/Teleport' export { Teleport, TeleportProps } from './components/Teleport'
export { Suspense, SuspenseProps } from './components/Suspense' export { Suspense, SuspenseProps } from './components/Suspense'

View File

@ -53,7 +53,7 @@ import {
queueEffectWithSuspense, queueEffectWithSuspense,
SuspenseImpl SuspenseImpl
} from './components/Suspense' } from './components/Suspense'
import { TeleportImpl } from './components/Teleport' import { TeleportImpl, TeleportVNode } from './components/Teleport'
import { isKeepAlive, KeepAliveContext } from './components/KeepAlive' import { isKeepAlive, KeepAliveContext } from './components/KeepAlive'
import { registerHMR, unregisterHMR, isHmrUpdating } from './hmr' import { registerHMR, unregisterHMR, isHmrUpdating } from './hmr'
import { import {
@ -477,8 +477,8 @@ function baseCreateRenderer(
) )
} else if (shapeFlag & ShapeFlags.TELEPORT) { } else if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).process( ;(type as typeof TeleportImpl).process(
n1, n1 as TeleportVNode,
n2, n2 as TeleportVNode,
container, container,
anchor, anchor,
parentComponent, parentComponent,

View File

@ -71,8 +71,14 @@ export type VNodeHook =
| VNodeMountHook[] | VNodeMountHook[]
| VNodeUpdateHook[] | VNodeUpdateHook[]
export interface VNodeProps { export interface ComponentCustomProps {}
[key: string]: any export interface AllowedComponentProps {
class?: unknown
style?: unknown
}
// https://github.com/microsoft/TypeScript/issues/33099
export type VNodeProps = {
key?: string | number key?: string | number
ref?: VNodeRef ref?: VNodeRef
@ -104,7 +110,11 @@ export type VNodeNormalizedChildren =
| RawSlots | RawSlots
| null | null
export interface VNode<HostNode = RendererNode, HostElement = RendererElement> { export interface VNode<
HostNode = RendererNode,
HostElement = RendererElement,
ExtraProps = { [key: string]: any }
> {
/** /**
* @internal * @internal
*/ */
@ -114,7 +124,7 @@ export interface VNode<HostNode = RendererNode, HostElement = RendererElement> {
*/ */
__v_skip: true __v_skip: true
type: VNodeTypes type: VNodeTypes
props: VNodeProps | null props: (VNodeProps & ExtraProps) | null
key: string | number | null key: string | number | null
ref: VNodeNormalizedRef | null ref: VNodeNormalizedRef | null
scopeId: string | null // SFC only scopeId: string | null // SFC only
@ -597,7 +607,7 @@ export function mergeProps(...args: (Data & VNodeProps)[]) {
const incoming = toMerge[key] const incoming = toMerge[key]
if (existing !== incoming) { if (existing !== incoming) {
ret[key] = existing ret[key] = existing
? [].concat(existing as any, toMerge[key]) ? [].concat(existing as any, toMerge[key] as any)
: incoming : incoming
} }
} else { } else {

View File

@ -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'))
}
}
})

View File

@ -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" />)

View File

@ -171,8 +171,9 @@ describe('with object props', () => {
eee={() => ({ a: 'eee' })} eee={() => ({ a: 'eee' })}
fff={(a, b) => ({ a: a > +b })} fff={(a, b) => ({ a: a > +b })}
hhh={false} hhh={false}
// should allow extraneous as attrs // should allow class/style as attrs
class="bar" class="bar"
style={{ color: 'red' }}
// should allow key // should allow key
key={'foo'} key={'foo'}
// should allow ref // should allow ref