Conversation
feat: composed async
feat: patterned templates
feat: ilc
feat: server side props
|
I'm worried that users can't use the function Comp(props) {
return <div>{props.foo}</div>
}If you destructure props, the remaining fields can be treated as attrs: function Comp({ foo, ...attrs}) {
return <div {...attrs}>{props.foo}</div>
}However, in Vapor mode, destructuring props would lose reactivity. So I create the @vue-jsx-vapor/macros, It compiles destructured props into a defineComponent(({ foo, ...attrs }) => {
return () => <div {...attrs}>{props.foo}</div>
})Compiles to: import { useProps } from 'vue-jsx-vapor'
defineComponent(() => {
const props = useProps() // currentInstance.props
const attrs = useAttrs() // rest prop
return () => <div {...attrs}>{props.foo}</div>
}, { props: ['foo'] }) |
|
One point I’d like to highlight is that attrs + useAttrs currently provides insufficient type checking and autocomplete support for component consumers. |
|
If all incoming values are accepted as props, it is somewhat unclear whether distinguishing between currentInstance.props and useAttrs is still necessary. |
Summary
Introduce a runtime option
nonValidatedPropsthat disables props filtering based on thepropsoption declaration. When enabled (true), the runtime no longer distinguishes betweenpropsandattrs— all passed attributes are treated uniformly as props. This allows components (especially in JSX/Vapor) to accept props purely through TypeScript function signatures, without requiring a compile-timepropsoption declaration.Additionally, a type-level
propsVarianceconfiguration is introduced via module augmentation to control the variance of component props types, defaulting to'contravariant'(to preserve fallthrough attrs behavior) with an optional'invariant'mode for stricter type checking. This is purely a compile-time concern with no runtime footprint.Basic example
Opting in to non-validated props
JSX Vapor with non-validated props
With
nonValidatedProps: true, components in JSX/Vapor can define props purely as function type parameters:Props variance control
Props variance is configured at the type level via module augmentation — no runtime setting is needed.
To opt into strict (invariant) mode, add a type declaration:
Motivation
Problem 1: AST-based
definePropsparsing is complex and fragileThe current Vue SFC compiler performs AST-level parsing of
definePropsto extract prop declarations at compile time. This involves:This approach is inherently limited. It cannot handle all valid TypeScript constructs (e.g., mapped types, conditional types, utility types referencing external modules). When it fails, developers encounter confusing compiler errors for code that is perfectly valid TypeScript.
Problem 2: TSX requires the
propsoption, hurting type ergonomicsIn TSX (and JSX) usage, Vue components must declare a
propsoption for the compiler to understand which attributes are props vs. attrs. This forces developers into a specific declaration style:This is redundant when TypeScript already provides a complete type system for describing function parameters. The
propsoption format — designed for runtime validation — is not a natural fit for TypeScript's structural type system.Problem 3: Poor compatibility with TypeScript's type system
Vue's runtime props system and TypeScript's type system have fundamental mismatches:
String,Number,Booleanas runtime constructors map awkwardly to TypeScript types.validatorfunction) has no type-level equivalent, leading to a dual-declaration problem.defaultandrequiredin the props option creates complex type inference rules that are difficult to understand and maintain.Goal
By making props filtering opt-out, we can:
nonValidatedProps: false) is unchanged. Existing components work exactly as before.Attrs as contravariant props
In Vue, attributes not declared in
propsfall through asattrsto the component's root element. This is a useful feature — it allows parent components to pass HTML attributes (class,style,id, event listeners, etc.) without the child needing to explicitly declare each one.When
nonValidatedPropsis enabled and there is nopropsdeclaration to filter against, all attributes are treated as props. The attrs fallthrough mechanism still works at the runtime level (undeclared props that match HTML attributes are applied to the root element).At the type level, this means the props type should be contravariant by default: a component that declares
{ title: string }should accept{ title: string; class?: string; id?: string; ... }without type errors. This preserves the attrs fallthrough ergonomics.For use cases where strict prop typing is desired (e.g., library components that want to catch extraneous props at compile time), the
propsVariance: 'invariant'option narrows the accepted props to exactly what is declared.Detailed design
Runtime behavior
nonValidatedPropsoptionA new boolean option, available at both the app level and per-component level:
When
nonValidatedPropsisfalse(default):propsoption declaration.attrs.When
nonValidatedPropsistrue:instance.propscontains all attributes passed to the component (there is no filtering based on a props declaration).instance.attrsbecomes an empty object (or is aliased toinstance.props, implementation detail).propsoption, if provided, is ignored for filtering purposes but can still be used for default values.Resolution order
Per-component
nonValidatedPropstakes precedence over the app-level setting:Impact on
useAttrs()and$attrsWhen
nonValidatedPropsistrue:useAttrs()returns an empty reactive object.$attrsin templates is an empty object.props(or$propsin templates).This is a conscious trade-off. Components that rely on
$attrsfor attribute forwarding (e.g.,v-bind="$attrs") should either:nonValidatedProps: false(the default).v-bind="$props"instead ofv-bind="$attrs"to forward all received attributes.Type system design
propsVariance— type-level configurationpropsVarianceis a purely type-level concern with no runtime representation. It controls how the TypeScript compiler checks extra attributes passed to components. Configuration is done via module augmentation, following the same pattern asvue-router's typed routes:'contravariant'(default — no configuration needed):The component accepts any superset of the declared props type. Extra attributes are allowed and handled by the runtime (fallthrough to root element, or ignored).
Type-level behavior:
This means:
'invariant':Opt in via module augmentation:
The component accepts only the exact declared props type. Extra attributes cause a type error.
Type-level behavior:
Note:
'invariant'is a type-level-only constraint. At runtime, extra attributes are still received (sincenonValidatedPropsdisables filtering) but are not used by the component. The type error serves as a development-time safeguard.Type inference for
defineVaporComponentThe props type
Pis inferred directly from thesetupfunction's parameter type. No additional type declaration is needed. The variance behavior is determined by the project-wideVuePropsConfiginterface (resolved at the type level via module augmentation).Compiler changes
SFC
<script setup>withnonValidatedPropsWhen a component opts into
nonValidatedProps, the SFC compiler behavior changes:definePropsbecomes optional. IfdefinePropsis not used, all attributes are available as props.definePropscan use arbitrary TypeScript types. Since the compiler no longer needs to extract runtime prop definitions from the type, complex types (mapped types, conditional types, imported types) work without restriction.propsarray or object.JSX / TSX
No compiler changes are needed for JSX/TSX. The type system handles everything:
definePropsgeneric.Interaction with existing features
defineEmitsdefineEmitsis unaffected. Event declarations remain separate from props.withDefaultswithDefaultscan still be used withdefinePropsto provide default values, even withnonValidatedProps: true. The defaults are applied at the runtime level.inheritAttrsWhen
nonValidatedPropsistrue,inheritAttrsbecomes a no-op (there are no attrs to inherit). Components that need attribute forwarding should usev-bind="$props".Transition from
nonValidatedProps: falsetotrueFor components migrating to
nonValidatedProps: true:v-bind="$attrs"withv-bind="$props"(or explicitly bind relevant props).useAttrs()with direct props access.propsoption can be removed if it was only used for type checking (and not for runtime validation or defaults).Drawbacks
attrsbecomes less useful. WithnonValidatedProps: true, the attrs concept effectively disappears. Components and libraries that rely on attrs separation (e.g., UI component libraries that use$attrsfor HTML attribute forwarding) cannot use this mode without refactoring.nonValidatedProps: trueand others don't, interoperability could be affected. Clear conventions are needed.propsoption provides runtime validation (type checking, required checks, custom validators) that is valuable during development. WithnonValidatedProps: true, these checks are skipped, and developers must rely solely on TypeScript for type safety.propsVarianceoption introduces type theory terminology that may confuse developers unfamiliar with variance concepts.Alternatives
1. Improve
definePropstype resolutionInstead of bypassing props validation, invest in making the SFC compiler's TypeScript type resolution more complete. This addresses Problem 1 but not Problems 2 or 3, and has diminishing returns as TypeScript's type system continues to grow in complexity.
2. Compiler-only approach (no runtime change)
Make the compiler smarter about TSX/JSX props without changing the runtime behavior. The compiler could emit a full
propsoption from TypeScript types, handling the translation automatically. This maintains runtime validation but still requires the compiler to understand arbitrary TypeScript types.3.
definePropswith pass-through modeInstead of a separate option, add a mode to
definePropsthat disables filtering:This is more localized but doesn't address the JSX/Vapor use case where
definePropsitself is the friction point.4. Separate component type for "function components"
Introduce a new component definition API specifically for simple function components:
This avoids changing existing behavior but fragments the component model further.
Adoption strategy
nonValidatedProps: false) is preserved. Existing applications are unaffected.nonValidatedPropsat the app level for new projects, or per-component for gradual migration.nonValidatedProps: truemode is the recommended default for Vapor-based applications, where components are closer to plain functions.propsoption to TypeScript-only props.$attrsusage with$props.VuePropsConfigwith'invariant'vs. the default'contravariant'.nonValidatedPropsmode:nonValidatedPropsistrue.VuePropsConfigmodule augmentation for JSX type checking.nonValidatedProps: trueto component options.$attrswith$props.propsoption declarations to TypeScript type parameters.Unresolved questions
nonValidatedPropsthe best name? Alternatives includerawProps,untypedProps,skipPropsValidation, orpropsMode: 'passthrough'. The name should clearly convey that props filtering is disabled.nonValidatedProps: true? This would make Vapor's default behavior different from the VDOM runtime, but it aligns with Vapor's functional component model.nonValidatedProps: true, what is the recommended pattern for attribute forwarding in wrapper components? Should a new utility (e.g.,splitProps()) be provided to separate "own" props from "forwarded" props?nonValidatedProps: true(no filtering) and runtime validation, should there be a way to add validation back (e.g., via avalidateoption or awithValidation()wrapper)?defineModel: How doesdefineModelbehave whennonValidatedPropsistrue? ThemodelValueprop is implicitly declared — should it still be special-cased?VuePropsConfigwith'contravariant'/'invariant'too academic? Alternatives like'open' | 'strict'or a simplestrictProps: booleaninterface might be more approachable.