
vue-tsx-support
TSX (JSX for TypeScript) support library for Vue
:warning: BREAKING CHANGES
If your project already uses vue-tsx-support v2, see Migration from V2 section.
TABLE OF CONTENTS
NEW FEATURES
- Typesafe emit for declared events
- @vue/composition-api support
PREREQUISITE
-
Vue >= 2.6.0, < 3.0.0
-
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.
INSTALLATION
-
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",
"..."
]
}
Migration from V2
-
In tsconfig.json
, set "VueTsxSupport"
to jsxFactory
-
Enable allow-props-object
option (Optional)
USAGE
Intrinsic elements
Standard HTML elements are defined as intrinsic elements.
So, compiler can check attribute names and attribute types of them:
<div id="title" />;
<input type="number" min={ 0 } max={ 100 } />;
<a href={ SOME_LINK } />;
<div href={ SOME_LINK } />;
<div id={ 1 } />;
Lower case tags are treated as unknown intrinsic element.
TypeScript checks nothing for such tags.
<foo id="unknown" unknownattr={ 1 } />
Components
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.
Make existing components tsx-ready.
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() {
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;
}
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 = {
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>
}}
/>
);
}
});
Writing components by object-style API (Like 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,
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({
});
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 {
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>
);
}
});
Writing component by class-style API (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.
1. Extends from 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.
2. Add _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!: {
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">>;
}
: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">>;
}
: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;
private count = 0;
private get className() {
return this.important ? "label-important" : "label-normal";
}
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 }>;
}
: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 }>;
}
Writing component by composition api (@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) => {
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>
);
}
});
OPTIONS
vue-tsx-support
has some options which change behaviour globally.
See under the options
directory.
To enable each options, import them somewhere
import "vue-tsx-support/options/allow-unknown-props";
:warning: Scope of option is whole project, not a file.
allow-element-unknown-attrs
Make enabled to specify unknown attributes to intrinsic elements
<div foo="foo" />;
allow-unknown-props
Make enabled to specify unknown props to Vue component.
const MyComponent = vuetsx.createComponent<{ foo: string }>({ });
<MyComponent foo="foo" bar="bar" />;
enable-html-attrs
Make enabled to specify HTML attributes to Vue component.
const MyComponent = vuetsx.createComponent<{ foo: string }>({ });
<MyComponent foo="foo" min={ 0 } max={ 100 } />;
<MyComponent foo="foo" min="a" />;
enable-nativeon
Make enabled to specify native event listeners to Vue component.
const MyComponent = vuetsx.createComponent<{ foo: string }>({ });
<MyComponent foo="foo" nativeOnClick={ e => ... } />;
enable-vue-router
Add definitions of router-link
and router-view
allow-props-object
Make enabled to pass props as "props".
const MyComponent = vuetsx.createComponent<{ foo: string }>({ });
<MyComponent props={{ foo: "foo" }} />;
APIS
modifiers
Event handler wrappers which work like some event modifiers available in template
import { modifiers as m } from "vue-tsx-support";
<div onKeydown={m.enter(this.onEnter)} />;
<div onKeydown={m.enter.prevent(this.onEnter)} />;
<div onKeydown={m.esc.prevent} />;
<div onKeydown={m.keys("enter", "esc")(this.onEnterOrEsc)} />;
<div onKeydown={m.keys(65).exact("ctrl", "alt")(this.onCtrlAltA)} />;
Available modifiers
-
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.
<div onKeydown={m.keys("enter", "esc")(handler)} />;
<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.
<div onClick={m.exact("ctrl", "shift")(handler)} />;
LICENSE
MIT