Skip to content

Commit 5d8a64d

Browse files
authored
feat(types): deny unknown attributes on component by default (#1614)
close #1519
1 parent 77659fa commit 5d8a64d

11 files changed

+100
-58
lines changed

packages/runtime-core/src/apiDefineComponent.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import {
1515
import { ExtractPropTypes, ComponentPropsOptions } from './componentProps'
1616
import { EmitsOptions } from './componentEmits'
1717
import { isFunction } from '@vue/shared'
18-
import { VNodeProps } from './vnode'
18+
import {
19+
VNodeProps,
20+
AllowedComponentProps,
21+
ComponentCustomProps
22+
} from './vnode'
1923

2024
// defineComponent is a utility that is primarily used for type inference
2125
// when declaring components. Type inference is provided in the component
@@ -40,7 +44,7 @@ export function defineComponent<Props, RawBindings = object>(
4044
{},
4145
{},
4246
// public props
43-
VNodeProps & Props
47+
VNodeProps & Props & AllowedComponentProps & ComponentCustomProps
4448
>
4549
> &
4650
FunctionalComponent<Props>
@@ -80,7 +84,7 @@ export function defineComponent<
8084
Mixin,
8185
Extends,
8286
E,
83-
VNodeProps & Props
87+
VNodeProps & Props & AllowedComponentProps & ComponentCustomProps
8488
>
8589
> &
8690
ComponentOptionsWithoutProps<
@@ -131,7 +135,8 @@ export function defineComponent<
131135
M,
132136
Mixin,
133137
Extends,
134-
E
138+
E,
139+
AllowedComponentProps & ComponentCustomProps
135140
>
136141
> &
137142
ComponentOptionsWithArrayProps<
@@ -182,7 +187,7 @@ export function defineComponent<
182187
Mixin,
183188
Extends,
184189
E,
185-
VNodeProps
190+
VNodeProps & AllowedComponentProps & ComponentCustomProps
186191
>
187192
> &
188193
ComponentOptionsWithObjectProps<

packages/runtime-core/src/components/Suspense.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ function patchSuspense(
197197
}
198198

199199
export interface SuspenseBoundary {
200-
vnode: VNode
200+
vnode: VNode<RendererNode, RendererElement, SuspenseProps>
201201
parent: SuspenseBoundary | null
202202
parentComponent: ComponentInternalInstance | null
203203
isSVG: boolean

packages/runtime-core/src/components/Teleport.ts

+8-9
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { VNode, VNodeArrayChildren, VNodeProps } from '../vnode'
1111
import { isString, ShapeFlags } from '@vue/shared'
1212
import { warn } from '../warning'
1313

14+
export type TeleportVNode = VNode<RendererNode, RendererElement, TeleportProps>
15+
1416
export interface TeleportProps {
1517
to: string | RendererElement
1618
disabled?: boolean
@@ -55,8 +57,8 @@ const resolveTarget = <T = RendererElement>(
5557
export const TeleportImpl = {
5658
__isTeleport: true,
5759
process(
58-
n1: VNode | null,
59-
n2: VNode,
60+
n1: TeleportVNode | null,
61+
n2: TeleportVNode,
6062
container: RendererElement,
6163
anchor: RendererNode | null,
6264
parentComponent: ComponentInternalInstance | null,
@@ -85,10 +87,7 @@ export const TeleportImpl = {
8587
insert(placeholder, container, anchor)
8688
insert(mainAnchor, container, anchor)
8789

88-
const target = (n2.target = resolveTarget(
89-
n2.props as TeleportProps,
90-
querySelector
91-
))
90+
const target = (n2.target = resolveTarget(n2.props, querySelector))
9291
const targetAnchor = (n2.targetAnchor = createText(''))
9392
if (target) {
9493
insert(targetAnchor, target)
@@ -165,7 +164,7 @@ export const TeleportImpl = {
165164
// target changed
166165
if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) {
167166
const nextTarget = (n2.target = resolveTarget(
168-
n2.props as TeleportProps,
167+
n2.props,
169168
querySelector
170169
))
171170
if (nextTarget) {
@@ -267,7 +266,7 @@ interface TeleportTargetElement extends Element {
267266

268267
function hydrateTeleport(
269268
node: Node,
270-
vnode: VNode,
269+
vnode: TeleportVNode,
271270
parentComponent: ComponentInternalInstance | null,
272271
parentSuspense: SuspenseBoundary | null,
273272
optimized: boolean,
@@ -284,7 +283,7 @@ function hydrateTeleport(
284283
) => Node | null
285284
): Node | null {
286285
const target = (vnode.target = resolveTarget<Element>(
287-
vnode.props as TeleportProps,
286+
vnode.props,
288287
querySelector
289288
))
290289
if (target) {

packages/runtime-core/src/h.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ type RawProps = VNodeProps & {
5050
__v_isVNode?: never
5151
// used to differ from Array children
5252
[Symbol.iterator]?: never
53-
}
53+
} & { [key: string]: any }
5454

5555
type RawChildren =
5656
| string

packages/runtime-core/src/hydration.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
SuspenseBoundary,
1919
queueEffectWithSuspense
2020
} from './components/Suspense'
21-
import { TeleportImpl } from './components/Teleport'
21+
import { TeleportImpl, TeleportVNode } from './components/Teleport'
2222

2323
export type RootHydrateFunction = (
2424
vnode: VNode<Node, Element>,
@@ -202,7 +202,7 @@ export function createHydrationFunctions(
202202
} else {
203203
nextNode = (vnode.type as typeof TeleportImpl).hydrate(
204204
node,
205-
vnode,
205+
vnode as TeleportVNode,
206206
parentComponent,
207207
parentSuspense,
208208
optimized,

packages/runtime-core/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export { h } from './h'
5454
// Advanced render function utilities
5555
export { createVNode, cloneVNode, mergeProps, isVNode } from './vnode'
5656
// VNode types
57-
export { Fragment, Text, Comment, Static } from './vnode'
57+
export { Fragment, Text, Comment, Static, ComponentCustomProps } from './vnode'
5858
// Built-in components
5959
export { Teleport, TeleportProps } from './components/Teleport'
6060
export { Suspense, SuspenseProps } from './components/Suspense'

packages/runtime-core/src/renderer.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import {
5353
queueEffectWithSuspense,
5454
SuspenseImpl
5555
} from './components/Suspense'
56-
import { TeleportImpl } from './components/Teleport'
56+
import { TeleportImpl, TeleportVNode } from './components/Teleport'
5757
import { isKeepAlive, KeepAliveContext } from './components/KeepAlive'
5858
import { registerHMR, unregisterHMR, isHmrUpdating } from './hmr'
5959
import {
@@ -477,8 +477,8 @@ function baseCreateRenderer(
477477
)
478478
} else if (shapeFlag & ShapeFlags.TELEPORT) {
479479
;(type as typeof TeleportImpl).process(
480-
n1,
481-
n2,
480+
n1 as TeleportVNode,
481+
n2 as TeleportVNode,
482482
container,
483483
anchor,
484484
parentComponent,

packages/runtime-core/src/vnode.ts

+15-5
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,14 @@ export type VNodeHook =
7171
| VNodeMountHook[]
7272
| VNodeUpdateHook[]
7373

74-
export interface VNodeProps {
75-
[key: string]: any
74+
export interface ComponentCustomProps {}
75+
export interface AllowedComponentProps {
76+
class?: unknown
77+
style?: unknown
78+
}
79+
80+
// https://github.com/microsoft/TypeScript/issues/33099
81+
export type VNodeProps = {
7682
key?: string | number
7783
ref?: VNodeRef
7884

@@ -104,7 +110,11 @@ export type VNodeNormalizedChildren =
104110
| RawSlots
105111
| null
106112

107-
export interface VNode<HostNode = RendererNode, HostElement = RendererElement> {
113+
export interface VNode<
114+
HostNode = RendererNode,
115+
HostElement = RendererElement,
116+
ExtraProps = { [key: string]: any }
117+
> {
108118
/**
109119
* @internal
110120
*/
@@ -114,7 +124,7 @@ export interface VNode<HostNode = RendererNode, HostElement = RendererElement> {
114124
*/
115125
__v_skip: true
116126
type: VNodeTypes
117-
props: VNodeProps | null
127+
props: (VNodeProps & ExtraProps) | null
118128
key: string | number | null
119129
ref: VNodeNormalizedRef | null
120130
scopeId: string | null // SFC only
@@ -597,7 +607,7 @@ export function mergeProps(...args: (Data & VNodeProps)[]) {
597607
const incoming = toMerge[key]
598608
if (existing !== incoming) {
599609
ret[key] = existing
600-
? [].concat(existing as any, toMerge[key])
610+
? [].concat(existing as any, toMerge[key] as any)
601611
: incoming
602612
}
603613
} else {

test-dts/componentTypeExtensions.test-d.ts

-30
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { defineComponent, expectError, expectType } from './index'
2+
3+
declare module '@vue/runtime-core' {
4+
interface ComponentCustomOptions {
5+
test?(n: number): void
6+
}
7+
8+
interface ComponentCustomProperties {
9+
state: 'stopped' | 'running'
10+
}
11+
12+
interface ComponentCustomProps {
13+
custom?: number
14+
}
15+
}
16+
17+
export const Custom = defineComponent({
18+
props: {
19+
bar: String,
20+
baz: {
21+
type: Number,
22+
required: true
23+
}
24+
},
25+
26+
data: () => ({ counter: 0 }),
27+
28+
test(n) {
29+
expectType<number>(n)
30+
},
31+
32+
methods: {
33+
aMethod() {
34+
// @ts-expect-error
35+
expectError(this.notExisting)
36+
this.counter++
37+
this.state = 'running'
38+
// @ts-expect-error
39+
expectError((this.state = 'not valid'))
40+
}
41+
}
42+
})
43+
44+
expectType<JSX.Element>(<Custom baz={1} />)
45+
expectType<JSX.Element>(<Custom custom={1} baz={1} />)
46+
expectType<JSX.Element>(<Custom bar="bar" baz={1} />)
47+
48+
// @ts-expect-error
49+
expectType<JSX.Element>(<Custom />)
50+
// @ts-expect-error
51+
expectError(<Custom bar="bar" />)
52+
// @ts-expect-error
53+
expectError(<Custom baz="baz" />)
54+
// @ts-expect-error
55+
expectError(<Custom baz={1} notExist={1} />)
56+
// @ts-expect-error
57+
expectError(<Custom baz={1} custom="custom" />)

test-dts/defineComponent.test-d.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,9 @@ describe('with object props', () => {
171171
eee={() => ({ a: 'eee' })}
172172
fff={(a, b) => ({ a: a > +b })}
173173
hhh={false}
174-
// should allow extraneous as attrs
174+
// should allow class/style as attrs
175175
class="bar"
176+
style={{ color: 'red' }}
176177
// should allow key
177178
key={'foo'}
178179
// should allow ref

0 commit comments

Comments
 (0)