How to Add Static Type Checking for Slots in Vue Components

Yangholmes
2025-05-28T07:30:25Z
This article was translated by deepseek, original article.
Under the premise of writing Vue components using TypeScript + JSX
1. Writing Vue Components with JSX Syntax
In addition to template syntax, Vue components can also be written using JSX. JSX, originally developed by Facebook for React, has gained popularity due to its ease of adoption, leading many web frameworks to support this JavaScript dialect. JSX also offers another advantage: seamless type integration (TSX). Any frontend framework that compiles templates into createElement()
-like functions should inherently support JSX, and Vue is no exception. Vue has supported JSX since version 2.x. The template options syntax in Vue 2 was cumbersome for flexible scenarios, often necessitating JSX or render function
implementations (e.g., ant-design-vue's Modal.method()).
Vue 3 introduced the setup
function, allowing JSX to be used not only in render functions but also directly in the return value of setup. Render functions tend to isolate component state, logic, lifecycle hooks, and structure, whereas the Composition API aims to bring these elements closer together. Thus, when using JSX in Vue 3, it is recommended to implement it within setup rather than a render function. A simple comparison:
// vue2
Vue.component('Hello', {
render: function (h) {
return h(
'h' + this.level,
this.title
)
},
props: {
level: {
type: Number,
required: true
},
title: {
type: String,
required: true
}
}
})
// vue3
import { ref, h } from 'vue'
export default {
props: {
level: {
type: Number,
required: true
},
title: {
type: String,
required: true
}
},
setup(props) {
return () => h('h' + props.level, props.title)
}
}
2. Adding Static Type Checks to Vue Components
Vue 2 had limited TypeScript support, making both template and render function
approaches cumbersome for large-scale projects. Vue 3 significantly enhances TypeScript integration, offering robust type inference for both the new <script setup>
syntax and traditional Options API. Previously, props validation was purely runtime-based. For example, defining a component's props:
props: {
title: {
type: String
}
}
If a non-String value was passed, a warning would appear in the browser console. This approach lacked static checks, making type errors prone in insufficiently tested code. Vue 3 improves this with minimal changes:
props: {
title: {
type: String as PropType<string>
}
}
Here, PropType
is a utility type provided by Vue for static type checking. For example, defining a Hello component:
import { defineComponent, h, PropType } from 'vue';
export const HelloProps = {
level: {
type: Number as PropType<1 | 2 | 3 | 4 | 5>,
required: true
},
title: {
type: String as PropType<string>,
required: true
}
} as const;
export default defineComponent({
name: 'Hello',
props: HelloProps,
setup(props) {
return () => h(
`h${props.level}`,
props.title,
);
}
});
Incorrect props now trigger static errors during development:
This approach is both elegant and type-safe. Similarly, Vue 3 provides defineSlots
for slot type checking in SFCs:
<script setup lang="ts">
const slots = defineSlots<{
default(props: { msg: string }): any
}>()
</script>
For non-<script setup>
components, the SlotsType
utility enforces slot types:
defineComponent({
name: 'B',
slots: Object as SlotsType<{
default: () => JSX.Element;
footer: () => JSX.Element;
}>,
setup(props, { slots }) {
return () => <>
{slots.default?.()}
</>
}
})
This provides type hints and checks for slots within setup. It is perfect ~ uh?
3. The Slots Type Dilemma in TSX
Despite defining slot types with SlotsType
, external usage of slots in TSX components remains unchecked. For example, using component B
with invalid slots does not trigger errors:
Why does this happen? While SFCs with and defineSlots work seamlessly, TSX components lack equivalent static checks.This raises the question: Can Vue 3 TSX components perform static type checks for slots?
4. Slots vs. Children
JSX transforms elements into createElement
-like calls. In React, children are treated as a props.children
attribute, but Vue handles slots differently. Vue’s JSX runtime does not map slots to props, and its type definitions (jsx-runtime.d.ts
) lack ElementChildrenAttribute
declarations. React’s approach:
interface ElementChildrenAttribute extends React.JSX.ElementChildrenAttribute {}
interface ElementChildrenAttribute {
children: {};
Vue’s JSX runtime omits this, preventing TypeScript from associating slots with props.
5. Solutions
i. Explicitly Define children
in Props
Manually declare slots as children
in props:
interface BSlots {
default?: () => JSX.Element,
footer?: () => JSX.Element
}
export const BProps = {
children: {
type: Object as PropType<BSlots>,
}
};
defineComponent({
name: 'B',
props: BProps,
slots: Object as SlotsType<BSlots>,
setup(props, { slots }) {
return () => <>
{slots.default?.()}
</>
}
})
This enforces slot type checks externally:
But What is the cost? Pollutes props with a non-standard children property and introduces naming confusion.
ii. Modify Vue’s JSX Runtime
Adjust the JSX type definitions to associate slots with children
. Update InferredProps
in Vue’s type declarations to include slots:
export function defineComponent<
// ... existing generics
ResolvedEmits extends EmitsOptions = {} extends RuntimeEmitsOptions
? TypeEmitsToOptions<TypeEmits>
: RuntimeEmitsOptions,
// 注入发生在这里,给 InferredProps 增加一个 children 属性,指向 传入的 Slots 定义,当然放到最后面会更好,不会被覆盖
InferredProps = {children?: UnwrapSlotsType<Slots>} & (IsKeyValues<TypeProps> extends true
? TypeProps
: string extends RuntimePropsKeys
? ComponentObjectPropsOptions extends RuntimePropsOptions
? {}
: ExtractPropTypes<RuntimePropsOptions>
: { [key in RuntimePropsKeys]?: any }),
TypeRefs extends Record<string, unknown> = {},
// ... remaining generics
>
This injects slot types into children
, enabling static checks. However, built-in Vue components (e.g., Fragment
) may require manual children
type fixes:
export declare const Fragment: {
__isFragment: true;
new (): {
$props: VNodeProps & {
children?: VNodeChild; // Add children type
};
};
};
What is the cost? Non-official solution requiring adjustments to internal types. Recommended only for projects fully committed to TSX. And, requires careful testing.