Awesome
Awesome Vue TS
Why:
Awesome Vue.TS aims at getting type safety as much as possible, while still keeping TypeScript concise and idiomatic. To achieve this, av-ts exploits many techniques, tricks and hacks in TypeScript, which makes av-ts a good tour of TypeScript features.
Note: The target vue version is 2.0.
You can read more for av-ts' raison d'etre here
Quick start
npm install vue-cli -g
vue init HerringtonDarkholme/av-ts-template myproject
cd myproject
npm install
npm run dev
Usage:
-
component is declared via a decorated class.
extends
should work. (mixin
support is added in 0.3.0!) -
data
,methods
andcomputed
can be declared by property initializer, member methods and property accessors in class body, respectively. -
props
are declared via@Prop
decorator and property initializer. -
render
and life cycle handler likecreated
is declared by decorated methods. They are declared in class body because these handlers usethis
. But you cannot invoke them on the instance itself. So they are decorated to remind users. When declaring custom methods, you should avoid these reserved names. -
watch
handlers are declared by@Watch(propName)
decorator, handler type is checked bykeyof
lookup type. -
All other options are considered as component's meta info. So users should declare them in the
@Component
decorator function. -
You can also extend av-ts by
Component.register
ing new decorators. Useful for libraries like Vuex, Vue-router.
Example:
import {
Component, Prop, Watch, Lifecycle, p
} from 'av-ts'
// vue options in `Component` decorator
@Component({
filters: {},
name: 'my-component',
delimiter: ['{{', '}}'],
})
export class MyComponent extends Vue { // extends Vue or your own component
// instance variable is in `data`
myData = '123'
// props declaration
@Prop myProp = p({
type: Object,
required: true,
default() {
return {a: 123, b: 456}
}
})
// method is `method`
myMethod() {
console.log('myMethod called!')
}
// accessor is `computed`
get myGetter() {
return this.myProp
}
// watch handler is declared by decorator
myWatchee = 'watch me!'
@Watch('myWatchee')
handler(newVal, oldVal) {
console.log(this.myWatchee + 'changed!')
}
// lifecycle hook is speical so it is decorated
@Lifecycle beforeCreate() {}
}
which is equivalent to
let MyComponent = Vue.extend({
filters: {},
name: 'my-component',
delimiter: ['{{', '}}'],
data() {
return {
myData: '123',
myWatchee: 'watch me!'
}
},
props: {
myProp: {
type: Object,
required: true,
default() {
return {a: 123, b: 456}
}
}
},
methods: {
myMethod() {
console.log('my method called!')
}
},
computed: {
myGetter: {
get() {
return this.myProp
}
}
},
watch: {
myWatchee() {
console.log(this.myWatchee + 'changed!')
}
},
beforeCreate() {}
})
mixin examples
Contrary to other libraries, av-ts supports first class Mixin! Example adapted from here
// define mixin trait by `Trait` decorator
@Trait class VegetableSearchable extends Vue {
vegetableName = 'tomato'
searchVegetable() { alert('find vegi!')}
}
@Trait class FruitSearchable extends Vue {
vegetableName = 'apple'
searchVegetable() { alert('find fruits!')}
}
// Mixin them!
@Component
class App extends Mixin(VegetableSearchable, FruitSearchable) {}
Voila! No implements
, No repeating code. And it looks like real mixins in ES6.
N.B.: Requires TypeScript 2.2.
TSX example
You need to first understand how TypeScript checkes JSX. https://www.typescriptlang.org/docs/handbook/jsx.html You also need to know the difference between VueJSX and React JSX. https://github.com/vuejs/babel-plugin-transform-vue-jsx
import Foo from './foo.vue'
@Component
class Bar extends Vue {
// $props is JSX.ElementAttributesProperty
$props: {
name: string
}
defaultName = 'John'
render() {
return (<Foo><Bar name={this.defaultName}>name attribute is required</Bar></Foo>)
}
}
API
For full type signature, please refer to av-ts.d.ts
. They are most up-to-date.
Class Decorators
Component
Type: ClassDecorator | (option) => ClassDecorator
It can be directly applied on component class as decorator, or take one option argument and return a decorator function.
@Component
class VueComp extends Vue {}
@Component({
directives: {},
components: {},
filters: {},
name: 'my-awesome-component',
delimiters: ['{{', '}}'],
})
class MyComponent extends Vue {}
Trait
Type: ClassDecorator | (option) => ClassDecorator
An alias of Component
,used for defining Vue traits to be mixed in.
At runtime, these decorators transform constructor to vue option and then feed to Vue.extend
.
So there is no semantic difference between Component
and Trait
. Placing Component
on a class to be used as mixin just feels too strange. This alias is solely for API aesthetic.
To use a Trait
, declare a class that extends Mixin(...Traits)
. See example in Mixin
section.
Property Decorators
Prop
Type: PropertyDecorator
Decorated properties should be the return value of utility function p
. p
is a function takes property option and return a fake type placeholder that will specify the property type. The fake type placeholder, at runtime, is just the config option object you feed to the argument.
@Prop
myProp = p({
type: Number,
default: 123
})
// p(option) returns a `number` type placeholder
// so the following code compiles
var num: number = p({
type: Number
})
// will print {type: Number}
console.log(num)
// you can also use a shorthand form of `p`
@Prop shortHand = p(String)
Watch
Same as vue-typescript, @Watch
is applied to a watched handler.
Watch
takes watchee name, or an array of key-path to a nested property, as the first argument, and an optional config object as the second one.
// watch handler is declared by decorator
properyBeingWatched = 123
@Watch('properyBeingWatched', {deep: true})
handler(newVal, oldVal) {
console.log('the delta is ' + (newVal - oldVal))
}
// the key path length is 4 at most
@Watch(['nested', 'path', 'property'])
handler(newVal, oldVal) {
console.log('the delta is ' + (newVal - oldVal))
}
// ....
is equivalent to
watch: {
properyBeingWatched: {
handler: function(newVal, oldVal) {
console.log('the delta is ' + (newVal - oldVal))
},
deep: true
}
}
Lifecycle
and Render
Type: TypedPropertyDecorator
mark decorated methods as special hooks in vue. Caveat: You cannot call lifecycle/render in other methods.
// lifecycle hook is speical so it is decorated
@Lifecycle mounted() {
console.log('called in lifecycle code!')
}
// this decorator can only decorate method with name same as lifecycle
// @Lifecycle willNotCompile() {}
Lifecycle from vue-router
is also supported as
import {
Component, Lifecycle, NextFunc, NextFuncVm, p, Prop
} from 'av-ts'
import { Route } from 'vue-router'
@Component({
name: 'my-page',
})
export default class MyPage extends Vue {
@Prop id = p({ type: String, required: true })
// "beforeRouteEnter" can use "NextFuncVm<T>"
@Lifecycle
async beforeRouteEnter(to: Route, from: Route, next: NextFuncVm<MyPage>) {
if (normalCase) {
next()
} else if (redirection) {
next({ name: 'other-page' })
} else if (cancel) {
next(false)
} else if (needCallback) {
next((vm) => {
console.log(vm.id)
})
}
}
// "beforeRouteUpdate" & "beforeRouteLeave" can only use "NextFunc"
@Lifecycle
async beforeRouteUpdate(to: Route, from: Route, next: NextFunc) {
if (normalCase) {
next()
} else if (redirection) {
next({ name: 'other-page' })
} else if (cancel) {
next(false)
}
}
}
Transition
Type: TypedPropertyDecorator
mark method as a callback of transition component. method is still called in other instance methods. This decorator is solely for type checking.
// solely for type checking! beforeEnter can be called in other methods
@Transition beforeEnter(el: HTMLElement) {
el.style.opacity = 0
el.style.height = 0
}
Data
Type: TypedPropertyDecorator
Collecting instance properties is heavy and hacky. It needs to find all props
and other properties for you. If you want to make instance creation faster you can skip data
collection. Here comes the Data
decorator. When Data
decorator is applied to a method, the method will be extracted as data
function in vue's option, with this
injected. And none instance property is counted as data
option.
Example:
@Component
class TestData extends Vue {
@Prop a = p(Number)
b = 456 // this initializer will be ignored
@Data data() {
return {
b: this.a // b will be initialized to prop value
}
}
}
let instance = new TestData({propsData: {a: 777}})
instance.b === 777 // true
Utility Functions
Mixin
has roughly type: <V>(parentConstructor: typeof Vue, ...traitConstructor: (typeof Vue)[]): {new(): V}
a function to mix all Trait
s decorated constructors into one Vue constructor.
To use Mixin correctly, you need to declare one interface to extend all traits you need. Then pass it as a generic type argument to Mixin<MixedInterface>(...traits)
. This is TypeScript's limitation.
In new version, you can just use Mixin(trait1, trait2)
. Note: Mixin supports at most four traits. More traits requires manuall type argument annotation.
It's return value is parentConstructor.extend({mixins: traitConstructor})
: extending the first trait as parentConstructor and pack all remaining traits in mixins
option.
See source for more specific type.
@Trait class Pen extends Vue {
havePen() { alert('I have a pen')}
}
@Trait class Apple extends Vue {
haveApple() { alert('I have an apple')}
}
// compiles under TS2.2
@Component class ApplePen extends Mixin(Apple, Pen) {
Uh() {
this.havePen()
this.haveApple()
alert('Apple pen')
}
}
is equivalent to
var Pen = Vue.extend({
methods: {
havePen() { alert('I have a pen')}
}
})
var Apple = Vue.extend({
methods: {
haveApple() { alert('I have an apple')}
}
})
var Mixin = Pen.extend({
mixins: [ Apple ]
})
var ApplePen = Mixin.extend({
methods: {
Uh() {
this.havePen()
this.haveApple()
alert('Apple pen')
}
}
})
Implementing PineapplePen
and PenPineappleApplePen
is left for exercise.
Explicit annotation example:
// Five traits and more reuire explicit annotation
interface GodLike extends FirstBlood, DoubleKill, KillingSpree, Rampage, Unstoppable {}
@Component
class LegendaryClass extends Mixin<GodLike>(FirstBlood, DoubleKill, KillingSpree, Rampage, Unstoppable) {
dominate() {
console.log('Mooooooooonster Kill')
}
}
Component.register
// has type
Component.register: (key: $$Prop, logic: DecoratorProcessor) => void
// $$Prop is a special string type that means you have to prefix the key with `$$`
// DecoratorProcessor can access prototype, instance and options of the decorated class
// where
type $$Prop = string & {'$$Prop Brand': never}
type DecoratorProcessor = (proto: Vue, instance: Vue, options: ComponentOptions<Vue>) => void;
Component.register
is for advanced users.
Sometimes you need to extend Vue's functionality by adding new instance option. Those new options usually are not type-safe. For example, render
is a special method can access this
but cannot be put in methods
option at the same time.
To implement a new decorator. You need first to know how av-ts works underhood. The comment is quite a good start. Also you can find some example implmentation.
common tricks
Please see FAQ
Difference
Added Feature:
- new decorator
@Transition
for typechecking transition hooks! - tsx support