В этой статье мы поговорим о Vue, TypeScript и TSX(JSX под TypeScript)
Основным преимуществом использования.tsx
— является проверка типа в функции render
.
Вот так будет выглядеть приложение, которое мы с вами создадим:

Исходный код этой статьи доступен здесь.
Установка
Устанавливаем Vue CLI v3, затем создаём новое приложение, запустив vue create tsx_adder
.
При установке выбираем:
- Необходимые зависимости — TypeScript и Babel
- Нам не нужен синтаксис компонента в стиле класса
- Babel
- Выбираем “in dedicated files”
Должно получится вот так:

Для использования TSX, нам понадобится зависимость — vue-tsx-support
. После завершения установки заходим в папку tsx_adder командой cd tsx_adder
. Устанавливаем пакет vue add tsx-support
.
Структура
В приложении будет два компонента: App.tsx
и Adder.tsx
. App.tsx
мы подключим к хранилищу Vuex и через входные параметры(props) будем передавать данные в Adder
, который будет отвечать за разметку и пользовательский интерфейс. Согласно шаблону компоненты-представления, раньше его называли «умные» и «глупые» компоненты, лучше создать AdderContainer.tsx
. Если кратко, компоненты-контейнеры отвечают за данные и операции с ними. Их состояние передается в виде свойств в компоненты-представления и отображается. Но для простоты, Adder.tsx
будет получать данные из хранилища через входные параметры(props) и передавать данные родительскому компоненту через встроенный метод $emit. Это позволит нам продемонстрировать:
- типизированные входные параметры(props), включая сложные типы, такие как Enums и Objects
- проверку типов событий между родительским/дочерним компонентом
- как получить тип интерфейс и тип безопасность с Vuex State в компонентах
Начнём с преобразования App.vue
в App.tsx
:
import * as tsx from 'vue-tsx-support'
import { VNode } from 'vue'
const App = tsx.component({
name: 'App',
render(): VNode {
return (
<Adder />
)
}
})
export { App }
Создадим файлAdder.tsx
: components/Adder.tsx
. Внутри components/Adder.tsx
добавляем компонент:
import * as tsx from 'vue-tsx-support'
import { VNode } from 'vue'
const Adder = tsx.component({
name: 'Adder',
render(): VNode {
return (
<div>Adder</div>
)
}
})
export { Adder }
Теперь импортируем его вApp.tsx
:
import { Adder } from './components/Adder'
. Направляемся в main.ts
и изменяем import App from './App.vue
на import { App } from './App'
. Запускаемyarn serve
(илиnpm run serve
). localhost:8080
должно получить:

Типизированные входные параметры(props)
Первое, что мы рассмотрим, это типизированные входные параметры(props), включающие как примитивные типы — Number
и Boolean
, так и сложные, например Enum
. В Adder.tsx
, добавляем следующее:
props: {
left: {
type: Number,
required: true as true
},
right: {
type: Number,
required: true as true
}
},
true as true
необходим TypeScriptу для проверки реквизитов во время компиляции.
Здесь это кратко описано. Если ваш редактор поддерживает TypeScript (например VS Code), в App.tsx
мы увидим ошибку, под Adder
появится красная линия. К концу сообщения об ошибке, говорится Type '{}' is missing the following properties from type '{ left: number; right: number; }': left, right
. Давайте добавим left
иright
вApp.tsx
:
render(): VNode {
return (
<Adder
left={5}
right={3}
/>
)
}
Если вместо этого передать строку — TypeScript предупредит нас, что тип реквизита неверен.
Далее добавим более сложный типenum
. Создаём каталог с именем types
в src
, а внутри него файлsign.ts
:
enum Sign {
'x' = 'x',
'/' = '/',
'+' = '+',
'-' = '-'
}
export { Sign }
Обновляем Adder.tsx
import { Sign } from '@/types/sign'
// ...
props: {
selectedSign: {
type: String as () => Sign,
required: true as true
}
}
Еще один хак,
который показывает некоторые ограничения поддержки Vue TS String as () => Sign
.
Поскольку перечисление Sign
это строки, мы вводим String as () => ...
.
Если бы это было перечисление Object
или Array
, мы бы ввели Array as () => MyComplexArrayType[]
.
Здесь об этом можно прочитать подробнее.
Возвращаемся в App.tsx
, и видим еще одну ошибку в <Adder />
. Исправляем:
// ...
import { Sign } from '@/types/sign'
// ...
render(): VNode {
return (
<Adder
left={5}
right={3}
selectedSign={Sign['+']}
/>
)
}
Типизированные события
Теперь давайте посмотрим, как проверить тип события. Мы хотим, чтобы калькулятор вызывал событие changeSign
при клике на любой из четырех знаков. Для этого используемcomponentFactoryOf
, документация здесь. Начнем с обновления App.tsx
:
// imports...
const App = tsx.component({
name: 'App',
methods: {
changeSign(sign: Sign) {
}
},
render(): VNode {
return (
<Adder
left={5}
right={3}
selectedSign={Sign['+']}
onChangeSign={this.changeSign}
/>
)
}
})
export { App }
<Adder />
снова ошибка:Property 'onChangeSign' does not exist on type '({ props: ...
. Это потому, что мы передаем параметр(props), который Adder
не ожидает.
Добавляем в Adder.tsx
:
interface IEvents {
onChangeSign: (sign: Sign) => void
}
const Adder = tsx.componentFactoryOf<IEvents>().create({
// ...
})
Теперь ошибка исчезла. Попробуйте изменитьchangeSign(sign: Sign)
наchangeSign(sign: Number)
— TS предупреждает, что параметр имеет неправильный тип. Подробнее о componentFactoryOf
здесь.
Для завершения нашего компонента Adder.tsx
добавим интерфейс, и функцию data
:
// imports ...
interface IAdderData {
signs: Sign[]
}
const Adder = tsx.componentFactoryOf<IEvents>().create({
// ...
data(): IAdderData {
return {
signs: [
Sign["+"],
Sign["-"],
Sign["x"],
Sign["/"]
]
}
}
Наконец, добавим функцию render
для Adder.tsx
.
render(): VNode { const { signs, left, right, selectedSign } = this return ( <div class='wrapper'> <div class='inner'> <div class='number'> {left} </div> <div class='signs'> {signs.map(sign => <span class={sign === selectedSign ? 'selected sign' : 'sign'} onClick={() => this.$emit('changeSign', sign)} > {sign} </span>) } </div> <div class='number'> {right} </div> </div> <div class='result'> <span> Result: {this.$slots.result} </span> </div> </div> ) }
Маленькая оговорка: мы определяем интерфейс события как onChangeSign
, но выделяем changeSign
.
Чтобы приложение выглядело получше, добавим немного стилей. Создаем components/adder.css
и вставляем следующее:
.wrapper, .signs {
display: flex;
flex-direction: column;
width: 200px;
}
.signs {
align-items: center;
}
.sign {
cursor: pointer;
border: 2px solid rgba(100, 100, 20, 0.4);
padding: 5px;
width: 30px;
height: 30px;
margin: 5px;
display: flex;
justify-content: center;
align-items: center;
}
.selected {
background-color: rgba(0, 0, 255, 0.4);
}
.inner {
display: flex;
justify-content: space-between;
}
.number {
font-size: 2.5em;
}
.result {
font-size: 3em;
}
Затемimport './adder.css'
сверхуAdder.tsx
. Теперь страница выглядит вот так:

Добавляем Vuex и кнопки начинают работать.
Добавление типизированного Vuex хранилища
Создаем папкуstore
внутриsrc
, затем вstore
создаемindex.ts
иcalculation.ts
. Внутриstore/index.ts
добавляем слудующее:
iimport Vue from 'vue'
import Vuex from 'vuex'
import { calculation, ICalculationState } from './calculation'
Vue.use(Vuex)
interface IState {
calculation: ICalculationState
}
const store = new Vuex.Store<IState>({
modules: {
calculation
}
})
export { store, IState }
Определяем новое Veux хранилище и передаем модульcalculation
. В calculation.ts
добавляем следующее:
import { Module } from 'vuex'
interface ICalculationState {
left: number
right: number
}
const calculation: Module<ICalculationState, {}> = {
state: {
left: 3,
right: 1
}
}
export {
calculation,
ICalculationState
}
Определяем модуль calculation
со значениями left
и right
. Импортируем его в main.ts
:
import Vue from 'vue'
import { App } from './App'
import { store } from '@/store'
Vue.config.productionTip = false
new Vue({
store,
render: h => h(App)
}).$mount('#app')
ОбновляемApp.tsx
:
import * as tsx from 'vue-tsx-support'
import { VNode } from 'vue'
import { Adder } from './components/Adder'
import { Sign } from '@/types/sign'
import { IState } from '@/store'
const App = tsx.component({
name: 'App',
computed: {
left(): number {
return (this.$store.state as IState).calculation.left
},
right(): number {
return (this.$store.state as IState).calculation.right
}
},
methods: {
changeSign(sign: Sign) {
}
},
render(): VNode {
return (
<Adder
left={this.left}
right={this.right}
selectedSign={Sign['+']}
onChangeSign={this.changeSign}
/>
)
}
})
export { App }
Добавляем (this.$store.state as IState)
для проверки типа модулей. Есть и другие альтернативы, которые позволят вам проверить тип без приведения this.$state
в State
, но мы будем использовать этот шаблон.
Добавление мутации
Нам необходимо сохранить selectedSign
в хранилище и обновить его мутацией. Обновляем calculation.ts
:
import { Module } from 'vuex'
import { Sign } from '@/types/sign'
interface ICalculationState {
left: number
right: number
sign: Sign
}
const SET_SIGN = 'SET_SIGN'
const calculation: Module<ICalculationState, {}> = {
namespaced: true,
state: {
left: 3,
right: 1,
sign: Sign['+']
},
mutations: {
[SET_SIGN](state, payload: Sign) {
state.sign = payload
}
}
}
export {
calculation,
ICalculationState
}
Мы добавили мутацию SET_SIGN
и будем использовать ее вApp.tsx
:
// ...
methods: {
changeSign(sign: Sign) {
this.$store.commit('calculation/SET_SIGN', sign)
}
},
// ...
Давайте доделаем наше приложение. В App.tsx
добавим вычисляемое свойство(computed property) result
для вычисления результата:
import * as tsx from 'vue-tsx-support'
import { VNode } from 'vue'
import { Adder } from './components/Adder'
import { Sign } from '@/types/sign'
import { IState } from '@/store'
const App = tsx.component({
name: 'App',
computed: {
left(): number {
return (this.$store.state as IState).calculation.left
},
right(): number {
return (this.$store.state as IState).calculation.right
},
sign(): Sign {
return (this.$store.state as IState).calculation.sign
},
result(): number {
switch (this.sign) {
case Sign['+']:
return this.left + this.right
case Sign['-']:
return this.left - this.right
case Sign['x']:
return this.left * this.right
case Sign['/']:
return this.left / this.right
}
}
},
methods: {
changeSign(sign: Sign) {
this.$store.commit('calculation/SET_SIGN', sign)
}
},
render(): VNode {
return (
<Adder
left={this.left}
right={this.right}
selectedSign={this.sign}
onChangeSign={this.changeSign}
>
<div slot='result'>
{this.result}
</div>
</Adder>
)
}
})
export { App }
Теперь наше приложение выглядит так:

Заключение
Эта статья демонстрирует:
- создание компонентов Vue с помощью
tsx
- типизация входных параметров(props) и событий
- использование интерфейсов в
this.$store
Статья прикольная, но поправьте футор на главной (гифка топ!!) его наверно надо к низу прижать, а то не кашерно смотрится, белая полоса на главной.