Browse Source

fix(types): narrow useAttrs class/style typing for TSX (#14492)

close #14489
edison 1 month ago
parent
commit
bbb89775b1

+ 16 - 1
packages-private/dts-test/tsx.test-d.tsx

@@ -1,5 +1,12 @@
 // TSX w/ defineComponent is tested in defineComponent.test-d.tsx
-import { Fragment, KeepAlive, Suspense, Teleport, type VNode } from 'vue'
+import {
+  Fragment,
+  KeepAlive,
+  Suspense,
+  Teleport,
+  type VNode,
+  useAttrs,
+} from 'vue'
 import { expectType } from './utils'
 
 expectType<VNode>(<div />)
@@ -54,6 +61,14 @@ expectType<JSX.Element>(
   />,
 )
 
+// allow class/style passthrough from attrs
+const attrs = useAttrs()
+expectType<JSX.Element>(<div class={attrs.class} />)
+expectType<JSX.Element>(<div style={attrs.style} />)
+
+// @ts-expect-error invalid class value
+;<div class={0} />
+
 // #7955
 expectType<JSX.Element>(<div style={[undefined, '', null, false]} />)
 

+ 11 - 4
packages/runtime-core/src/component.ts

@@ -98,6 +98,13 @@ import type { RendererElement } from './renderer'
 
 export type Data = Record<string, unknown>
 
+/**
+ * For extending allowed non-declared attrs on components in TSX
+ */
+export interface AllowedAttrs {}
+
+export type Attrs = Data & AllowedAttrs
+
 /**
  * Public utility type for extracting the instance type of a component.
  * Works with all valid component definition types. This is intended to replace
@@ -283,7 +290,7 @@ export type SetupContext<
   S extends SlotsType = {},
 > = E extends any
   ? {
-      attrs: Data
+      attrs: Attrs
       slots: UnwrapSlotsType<S>
       emit: EmitFn<E>
       expose: <Exposed extends Record<string, any> = Record<string, any>>(
@@ -1152,13 +1159,13 @@ export function createSetupContext(
   if (__DEV__) {
     // We use getters in dev in case libs like test-utils overwrite instance
     // properties (overwrites should not be done in prod)
-    let attrsProxy: Data
+    let attrsProxy: Attrs
     let slotsProxy: Slots
     return Object.freeze({
       get attrs() {
         return (
           attrsProxy ||
-          (attrsProxy = new Proxy(instance.attrs, attrsProxyHandlers))
+          (attrsProxy = new Proxy(instance.attrs, attrsProxyHandlers) as Attrs)
         )
       },
       get slots() {
@@ -1171,7 +1178,7 @@ export function createSetupContext(
     })
   } else {
     return {
-      attrs: new Proxy(instance.attrs, attrsProxyHandlers),
+      attrs: new Proxy(instance.attrs, attrsProxyHandlers) as Attrs,
       slots: instance.slots,
       emit: instance.emit,
       expose,

+ 2 - 1
packages/runtime-core/src/componentPublicInstance.ts

@@ -1,4 +1,5 @@
 import {
+  type Attrs,
   type Component,
   type ComponentInternalInstance,
   type Data,
@@ -306,7 +307,7 @@ export type ComponentPublicInstance<
   $props: MakeDefaultsOptional extends true
     ? Partial<Defaults> & Omit<Prettify<P> & PublicProps, keyof Defaults>
     : Prettify<P> & PublicProps
-  $attrs: Data
+  $attrs: Attrs
   $refs: Data & TypeRefs
   $slots: UnwrapSlotsType<S>
   $root: ComponentPublicInstance | null

+ 2 - 0
packages/runtime-core/src/index.ts

@@ -259,7 +259,9 @@ export type {
   ConcreteComponent,
   FunctionalComponent,
   ComponentInternalInstance,
+  Attrs,
   SetupContext,
+  AllowedAttrs,
   ComponentCustomProps,
   AllowedComponentProps,
   GlobalComponents,

+ 6 - 0
packages/runtime-dom/src/index.ts

@@ -33,6 +33,7 @@ import type { TransitionGroupProps } from './components/TransitionGroup'
 import type { vShow } from './directives/vShow'
 import type { VOnDirective } from './directives/vOn'
 import type { VModelDirective } from './directives/vModel'
+import type { ClassValue, StyleValue } from './jsx'
 
 /**
  * This is a stub implementation to prevent the need to use dom types.
@@ -48,6 +49,11 @@ declare module '@vue/reactivity' {
 }
 
 declare module '@vue/runtime-core' {
+  interface AllowedAttrs {
+    class?: ClassValue
+    style?: StyleValue
+  }
+
   interface GlobalComponents {
     Transition: DefineComponent<TransitionProps>
     TransitionGroup: DefineComponent<TransitionGroupProps>