TSX (JSX for TypeScript) support library for Vue
If your project already uses vue-tsx-support v2, see Migration from V2 section.
TypeScript >= 3.8.0 (3.9.2 or later recommended)
vue-tsx-support
does not support Vue 3 because Vue 3 has it's own JSX type checker and there are some incompatibilities with Vue 2.
If you want to use composition API with vue-tsx-support
, you can use @vue/composition-api.
Create Vue project with TypeScript and babel support.
vue-tsx-support is a type checker for TypeScript, not a transpiler.
You must install babel presets (@vue/babel-preset-app or @vue/babel-preset-jsx) separatedly.
Vue CLI may help you.
:bulb: If you want use @vue/composition-api, @vue/babel-preset-jsx
>= 1.2.1 or babel-preset-vue-vca is needed.
Install vue-tsx-support from npm
yarn add vue-tsx-support -D
In tsconfig.json
, set "preserve"
to jsx
and "VueTsxSupport"
to jsxFactory
{
"compilerOptions": {
"jsx": "preserve",
"jsxFactory": "VueTsxSupport",
"...": "..."
},
"include": [
"..."
]
}
import vue-tsx-support/enable-check.d.ts
somewhere,
import "vue-tsx-support/enable-check"
or add it to "include" in tsconfig.json
{
"compilerOptions": {
"...": "..."
},
"include": [
"node_modules/vue-tsx-support/enable-check.d.ts",
"..."
]
}
In tsconfig.json
, set "VueTsxSupport"
to jsxFactory
Enable allow-props-object
option (Optional)
Standard HTML elements are defined as intrinsic elements. So, compiler can check attribute names and attribute types of them:
// OK
<div id="title" />;
// OK
<input type="number" min={ 0 } max={ 100 } />;
// OK
<a href={ SOME_LINK } />;
// NG: because `href` is not a valid attribute of `div`
<div href={ SOME_LINK } />;
// NG: because `id` must be a string
<div id={ 1 } />;
Lower case tags are treated as unknown intrinsic element. TypeScript checks nothing for such tags.
// OK
<foo id="unknown" unknownattr={ 1 } />
Basically, vue-tsx-support
checks three types for each component.
Prop types
Determine name, type, and required or optional of each props.
When using existing component as-is, you must specify prop types manually.
When writing component with APIs of vue-tsx-support
, prop types are automatically obtained from component definition.
Custom event types (optional)
If the component has custom events, you can specify custom event types additionally,
and vue-tsx-support
will check if event names and argument types are correct or not.
Scoped slot types (optional)
If the component has uses scoped slots, you can specify scoped slot types additionally,
and vue-tsx-support
will check if scoped slot names and argument types are correct or not.
By default, vue-tsx-support
does not allow unknown props.
For example, below code causes compilation error.
import Vue from "vue";
import AwesomeButton from "third-party-library/awesome-button";
export default Vue.extend({
render() {
// ERROR: because TypeScript does not know that AwesomeButton has 'text' prop.
return <AwesomeButton text="Click Me!" />;
}
});
You can add type information to existing component without modifying component itself, like below:
import AwesomeButtonOrig from "third-party-library/awesome-button";
import * as tsx from "vue-tsx-support";
type AwesomeButtonProps = {
text: string;
raised?: boolean;
rounded?: boolean;
}
// Now, AwesomeButton has 1 required prop(text) and 2 optional props(raised, rounded)
export const AwesomeButton = tsx.ofType<AwesomeButtonProps>().convert(AwesomeButtonOrig);
You also can specify custom event types as second type parameter, and scoped slot types as third type parameter.
For example:
import AwesomeListOrig from "third-party-library/awesome-list";
import * as tsx from "vue-tsx-support";
type Item = { id: string, text: string };
type AwesomeListProps = {
items: ReadonlyArray<Item>;
rowHeight: number;
}
type AwesomeListEvents = {
// member name must be ['on' + event name(with capitalizing first charactor)]
onRowClicked: { item: Item, index: number };
}
type AwesomeListScopedSlots = {
row: { item: Item }
}
export const AwesomeList = tsx.ofType<
AwesomeListProps,
AwesomeListEvents,
AwesomeListScopedSlots
>().convert(AwesomeListOrig);
Then you can use AwesomeList like below:
import { VNode } from "vue";
const App = Vue.extend({
render(): VNode {
return (
<AwesomeList
items={this.items}
rowHeight={32}
onRowClicked={p => console.log(`${p.item.text} clicked!`)}
scopedSlots={{
row: item => <div>{item.text}</div>
}}
/>
);
}
});
Vue.extend
)If you use Vue.extend()
, just replace it by componentFactory.create
and your component becomes TSX-ready.
Props type is infered from props definition automatically.
For example, props type will be { text: string, important?: boolean }
in below code.
:warning: In some environment, as const
may be needed to make prop required properly.
import { VNode } from "vue";
import * as tsx from "vue-tsx-support";
const MyComponent = tsx.componentFactory.create({
props: {
text: { type: String, required: true },
important: Boolean,
} as const, // `as const` is needed in some cases.
computed: {
className(): string {
return this.important ? "label-important" : "label-normal";
}
},
methods: {
onClick(event: Event) { this.$emit("ok", event); }
},
render(): VNode {
return <span class={this.className} onClick={this.onClick}>{this.text}</span>;
}
});
:bulb: You can use component
as as shorthand of componentFactory.create
.
import * as tsx from "vue-tsx-support";
const MyComponent = tsx.component({
/* snip */
});
If your component has custom events or scoped slots, use componentFactoryOf
instead.
import { VNode } from "vue";
import * as tsx from "vue-tsx-support";
type AwesomeListEvents = {
onRowClicked: { item: {}, index: number };
}
type AwesomeListScopedSlots = {
row: { item: {} }
}
export const AwesomeList = tsx.componentFactoryOf<
AwesomeListEvents,
AwesomListScopedSlots
>().create({
name: "AwesomeList",
props: {
items: { type: Array, required: true },
rowHeight: { type: Number, required: true }
},
computed: { /* ... */},
method: {
emitRowClicked(item: {}, index: number): void {
// Equivalent to `this.$emit("rowClicked", { item, index })`,
// And event name and payload type are statically checked.
tsx.emitOn(this, "onRowClicked", { item, index });
}
},
render(): VNode {
return (
<div class={style.container}>
{
this.visibleItems.map((item, index) => (
<div style={this.rowStyle} onClick={() => this.$emit("rowClicked", { item, index })}>
{
// slot name ('row') and argument types are statically checked.
this.$scopedSlots.row({ item })
}
<div>
)
}
</div>
);
}
});
vue-class-component
and/or vue-property-decorator
)If you prefer class-style component by using vue-class-component
and/or vue-property-decorator
,
there are some options to make it tsx-ready.
Component
class provided by vue-tsx-support
import { VNode } from "vue";
import { Component, Prop } from "vue-property-decorator";
import * as tsx from "vue-tsx-support";
type MyComponentProps = {
text: string;
important?: boolean;
}
@Component
export class MyComponent extends tsx.Component<MyComponentProps> {
@Prop({ type: String, required: true })
text!: string;
@Prop(Boolean)
important?: boolean;
get className() {
return this.important ? "label-important" : "label-normal";
}
onClick(event: MouseEvent) {
this.$emit("ok", event);
}
render(): VNode {
return <span class={this.className} onClick={this.onClick}>{this.text}</span>;
}
}
:warning: Unfortunately, vue-tsx-support
can't infer prop types automatically in this case, so you must write type manually.
_tsx
field to tell type information to TypeScript. import { VNode } from "vue";
import { Component, Prop } from "vue-property-decorator";
import * as tsx from "vue-tsx-support";
@Component
export class MyComponent extends Vue {
_tsx!: {
// specify props type to `props`.
props: Pick<MyComponent, "text" | "important">
};
@Prop({ type: String, required: true })
text!: string;
@Prop(Boolean)
important?: boolean;
get className() {
return this.important ? "label-important" : "label-normal";
}
render(): VNode {
return <span class={this.className}>{this.text}</span>;
}
}
You can use DeclareProps<T>
instead of { props: T }
.
import { Component, Prop } from "vue-property-decorator";
import * as tsx from "vue-tsx-support";
@Component
export class MyComponent extends Vue {
_tsx!: tsx.DeclareProps<Pick<MyComponent, "text" | "important">>;
/* ...snip... */
}
:bulb: PickProps
is more convenient than Pick
here, it removes attributes from Vue
from completion candidates. (e.g. $data
, $props
, and so on)
import { Component, Prop } from "vue-property-decorator";
import * as tsx from "vue-tsx-support";
@Component
export class MyComponent extends Vue {
_tsx!: tsx.DeclareProps<tsx.PickProps<MyComponent, "text" | "important">>;
/* ...snip... */
}
:bulb: When you can make all data, computed and methods private, you can use AutoProps
instead.
AutoProps
picks all public members other than members from component options(render
, created
etc).
import { Component, Prop } from "vue-property-decorator";
import * as tsx from "vue-tsx-support";
@Component
export class MyComponent extends Vue {
_tsx!: tsx.DeclareProps<tsx.AutoProps<MyComponent>>
@Prop({ type: String, required: true })
text!: string;
@Prop(Boolean)
important?: boolean;
// data
private count = 0;
// computed
private get className() {
return this.important ? "label-important" : "label-normal";
}
// methods
private onClick() {
this.count += 1;
}
render(): VNode {
return (
<span class={this.className} onClick={this.onClick}>
{`${this.text}-${this.count}`}
</span>
);
}
}
:bulb: If your component has custom events, you can specify events handlers type additionally.
import { Component, Prop } from "vue-property-decorator";
import * as tsx from "vue-tsx-support";
@Component
export class MyComponent extends Vue {
_tsx!: tsx.DeclareProps<PickProps<MyComponent, "text" | "important">> &
tsx.DeclareOnEvents<{ onOk: string }>;
/* ...snip... */
}
:bulb: If your component uses scoped slots, you should add type to $scopedSlots
by tsx.InnerScopedSlots
.
import { Component, Prop } from "vue-property-decorator";
import * as tsx from "vue-tsx-support";
@Component
export class MyComponent extends Vue {
_tsx!: tsx.DeclareProps<PickProps<MyComponent, "text" | "important">>;
$scopedSlots!: tsx.InnerScopedSlots<{ default?: string }>;
/* ...snip... */
}
@vue/composition-api
)Vue 3 is not supported.
To use composition api with Vue 2, You can use @vue/composition-api
.
There are 2 babel presets which support JSX syntax with @vue/composition-api
.
@vue/babel-preset-jsx
>= 1.2.1 (You must enable composition-api support explicitly by specifying { compositionAPI: true }
)babel-preset-vca-jsx
To make TSX-ready component by composition api, use component
of vue-tsx-support/lib/vca
instead of defineComponent
of @vue/composition-api
.
import { computed } from "@vue/composition-api";
import * as vca from "vue-tsx-support/lib/vca";
const MyComponent = vca.component({
name: "MyComponent",
props: {
text: { type: String, required: true },
important: Boolean,
},
setup(p) {
const className = computed(() => p.important ? "label-important" : "label-normal");
return () => (
<span class={className.value}>{p.text}</span>;
);
}
});
If your component has custom event or scoped slots, specify them types in 2nd argument of setup
.
import { computed, onMounted } from "@vue/composition-api";
import * as vca from "vue-tsx-support/lib/vca";
type AwesomeListEvents = {
onRowClicked: { item: {}, index: number };
}
type AwesomeListScopedSlots = {
row: { item: {} }
}
export const AwesomeList = vca.component({
name: "AwesomeList",
props: {
items: { type: Array, required: true },
rowHeight: { type: Number, required: true }
},
setup(p, ctx: vca.SetupContext<AwesomeListEvents, AwesomeListScopedSlots>) {
const visibleItems = computed(() => ... );
const emitRowClicked = (item: {}, index: number) => {
// Equivalent to `ctx.emit("rowClicked", { item, index })`,
// And event name and payload type are statically checked.
vca.emitOn(ctx, "onRowClicked", { item, index });
}
return () => (
<div class={style.container}>
{
visibleItems.value.map((item, index) => (
<div onClick={() => emitRowClicked(item, index)}>
{
// slot name ('row') and argument types are statically checked.
ctx.slots.row({ item })
}
<div>
)
}
</div>
);
}
});
vue-tsx-support
has some options which change behaviour globally.
See under the options
directory.
To enable each options, import them somewhere
// enable `allow-unknown-props` option
import "vue-tsx-support/options/allow-unknown-props";
:warning: Scope of option is whole project, not a file.
Make enabled to specify unknown attributes to intrinsic elements
// OK:`foo` is unknown attribute, but can be compiled
<div foo="foo" />;
Make enabled to specify unknown props to Vue component.
const MyComponent = vuetsx.createComponent<{ foo: string }>({ /* ... */ });
// OK: `bar` is unknown prop, but can be compiled
<MyComponent foo="foo" bar="bar" />;
Make enabled to specify HTML attributes to Vue component.
const MyComponent = vuetsx.createComponent<{ foo: string }>({ /* ... */ });
// OK: `min` and `max` are valid HTML attributes
<MyComponent foo="foo" min={ 0 } max={ 100 } />;
// NG: compiler checks type of `min` (`min` must be number)
<MyComponent foo="foo" min="a" />;
Make enabled to specify native event listeners to Vue component.
const MyComponent = vuetsx.createComponent<{ foo: string }>({ /* ... */ });
// OK
<MyComponent foo="foo" nativeOnClick={ e => ... } />; // and `e` is infered as MouseEvent
Add definitions of router-link
and router-view
Make enabled to pass props as "props".
const MyComponent = vuetsx.createComponent<{ foo: string }>({ /* ... */ });
// OK
<MyComponent props={{ foo: "foo" }} />;
Event handler wrappers which work like some event modifiers available in template
import { modifiers as m } from "vue-tsx-support";
// Basic usage:
// Equivalent to `<div @keydown.enter="onEnter" />`
<div onKeydown={m.enter(this.onEnter)} />;
// Use multiple modifiers:
// Equivalent to `<div @keydown.enter.prevent="onEnter" />`
<div onKeydown={m.enter.prevent(this.onEnter)} />;
// Use without event handler:
// Equivalent to `<div @keydown.esc.prevent />`
<div onKeydown={m.esc.prevent} />;
// Use multiple keys:
// Equivalent to `<div @keydown.enter.esc="onEnterOrEsc" />`
<div onKeydown={m.keys("enter", "esc")(this.onEnterOrEsc)} />;
// Use exact modkey combination:
// Equivalent to `<div @keydown.65.ctrl.alt.exact="onCtrlAltA" />`
<div onKeydown={m.keys(65).exact("ctrl", "alt")(this.onCtrlAltA)} />;
esc
, tab
, enter
, space
, up
, down
, del
, left
, right
Execute event handler only when specified key is pressed.
:warning: del
allows not only DELETE, but also BACKSPACE.
:warning: left
and right
have another behavior when specified to mouse event
:warning: combination of key modifiers (e.g. m.enter.esc
) does not work. See keys
left
, right
, middle
Execute event handler only when specified mouse button is pressed.
:warning: left
and right
have another behavior when specified to keyboard event
ctrl
, shift
, alt
, meta
Execute event handler only when specified system modifier key is pressed.
noctrl
, noshift
, noalt
, nometa
Execute event handler only when specified system modifier key is not pressed.
self
Execute event handler only when event.target is the element itself (not from children).
prevent
, stop
Call preventDefault
or stopPropagation
of event object before executing event handler.
keys(...args)
Execute event handler only when one of specified key is pressed.
Known key name("esc", "tab", "enter", ...) or number can be specified.
// when enter or esc pressed
<div onKeydown={m.keys("enter", "esc")(handler)} />;
// when 'a' pressed
<div onKeydown={m.keys(65)(handler)} />;
exact(...args)
Execute event handler only when specified system modifier keys are all pressed, and others are not pressed.
// when CTRL, SHIFT are both pressed, and ALT, META are both not pressed
<div onClick={m.exact("ctrl", "shift")(handler)} />;
MIT