diff --git a/package.json b/package.json index ec19e520..b9a9d005 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "eslint": "^3.0.0", "eslint-loader": "^1.7.1", "eslint-plugin-flowtype": "^2.34.0", - "eslint-plugin-jasmine": "^2.2.0", + "eslint-plugin-jasmine": "^2.8.4", "eslint-plugin-vue-libs": "^1.2.0", "file-loader": "^0.11.2", "flow-bin": "^0.54.0", @@ -119,7 +119,7 @@ "selenium-server": "^2.53.1", "serialize-javascript": "^1.3.0", "shelljs": "^0.7.8", - "typescript": "^2.3.4", + "typescript": "^2.5.2", "uglify-js": "^3.0.15", "webpack": "^2.6.1", "weex-js-runtime": "^0.22.0" diff --git a/types/index.d.ts b/types/index.d.ts index 28350fcf..da58517f 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,37 +1,36 @@ -import * as V from "./vue"; -import * as Options from "./options"; -import * as Plugin from "./plugin"; -import * as VNode from "./vnode"; +import { Vue } from "./vue"; -// `Vue` in `export = Vue` must be a namespace -// All available types are exported via this namespace -declare namespace Vue { - export type CreateElement = V.CreateElement; +export default Vue; - export type Component = Options.Component; - export type AsyncComponent = Options.AsyncComponent; - export type ComponentOptions = Options.ComponentOptions; - export type FunctionalComponentOptions = Options.FunctionalComponentOptions; - export type RenderContext = Options.RenderContext; - export type PropOptions = Options.PropOptions; - export type ComputedOptions = Options.ComputedOptions; - export type WatchHandler = Options.WatchHandler; - export type WatchOptions = Options.WatchOptions; - export type DirectiveFunction = Options.DirectiveFunction; - export type DirectiveOptions = Options.DirectiveOptions; +export { + CreateElement +} from "./vue"; - export type PluginFunction = Plugin.PluginFunction; - export type PluginObject = Plugin.PluginObject; +export { + Component, + AsyncComponent, + ComponentOptions, + FunctionalComponentOptions, + RenderContext, + PropOptions, + ComputedOptions, + WatchHandler, + WatchOptions, + WatchOptionsWithHandler, + DirectiveFunction, + DirectiveOptions +} from "./options"; - export type VNodeChildren = VNode.VNodeChildren; - export type VNodeChildrenArrayContents = VNode.VNodeChildrenArrayContents; - export type VNode = VNode.VNode; - export type VNodeComponentOptions = VNode.VNodeComponentOptions; - export type VNodeData = VNode.VNodeData; - export type VNodeDirective = VNode.VNodeDirective; -} +export { + PluginFunction, + PluginObject +} from "./plugin"; -// TS cannot merge imported class with namespace, declare a subclass to bypass -declare class Vue extends V.Vue {} - -export = Vue; +export { + VNodeChildren, + VNodeChildrenArrayContents, + VNode, + VNodeComponentOptions, + VNodeData, + VNodeDirective +} from "./vnode"; diff --git a/types/options.d.ts b/types/options.d.ts index b28a228a..7cba394e 100644 --- a/types/options.d.ts +++ b/types/options.d.ts @@ -1,48 +1,88 @@ -import { Vue, CreateElement } from "./vue"; +import { Vue, CreateElement, CombinedVueInstance } from "./vue"; import { VNode, VNodeData, VNodeDirective } from "./vnode"; type Constructor = { new (...args: any[]): any; } -export type Component = typeof Vue | ComponentOptions | FunctionalComponentOptions; +// we don't support infer props in async component +export type Component, Methods=DefaultMethods, Computed=DefaultComputed, Props=DefaultProps> = + | typeof Vue + | FunctionalComponentOptions + | ThisTypedComponentOptionsWithArrayProps + | ThisTypedComponentOptionsWithRecordProps; interface EsModuleComponent { default: Component } -export type AsyncComponent = ( - resolve: (component: Component) => void, +export type AsyncComponent, Methods=DefaultMethods, Computed=DefaultComputed, Props=DefaultProps> = ( + resolve: (component: Component) => void, reject: (reason?: any) => void -) => Promise | Component | void; +) => Promise | void; + +/** + * When the `Computed` type parameter on `ComponentOptions` is inferred, + * it should have a property with the return type of every get-accessor. + * Since there isn't a way to query for the return type of a function, we allow TypeScript + * to infer from the shape of `Accessors` and work backwards. + */ +export type Accessors = { + [K in keyof T]: (() => T[K]) | ComputedOptions +} -export interface ComponentOptions { - data?: Object | ((this: V) => Object); - props?: string[] | { [key: string]: PropOptions | Constructor | Constructor[] }; +/** + * This type should be used when an array of strings is used for a component's `props` value. + */ +export type ThisTypedComponentOptionsWithArrayProps = + object & + ComponentOptions> & V) => Data), Methods, Computed, PropNames[]> & + ThisType>>>; + +/** + * This type should be used when an object mapped to `PropOptions` is used for a component's `props` value. + */ +export type ThisTypedComponentOptionsWithRecordProps = + object & + ComponentOptions & V) => Data), Methods, Computed, RecordPropsDefinition> & + ThisType>>; + +type DefaultData = object | ((this: V) => object); +type DefaultProps = Record; +type DefaultMethods = { [key: string]: (this: V, ...args: any[]) => any }; +type DefaultComputed = { [key: string]: any }; +export interface ComponentOptions< + V extends Vue, + Data=DefaultData, + Methods=DefaultMethods, + Computed=DefaultComputed, + PropsDef=PropsDefinition> { + data?: Data; + props?: PropsDef; propsData?: Object; - computed?: { [key: string]: ((this: V) => any) | ComputedOptions }; - methods?: { [key: string]: (this: V, ...args: any[]) => any }; - watch?: { [key: string]: ({ handler: WatchHandler } & WatchOptions) | WatchHandler | string }; + computed?: Accessors; + methods?: Methods; + watch?: Record | WatchHandler | string>; el?: Element | String; template?: string; - render?(this: V, createElement: CreateElement): VNode; + render?(createElement: CreateElement): VNode; renderError?: (h: () => VNode, err: Error) => VNode; staticRenderFns?: ((createElement: CreateElement) => VNode)[]; beforeCreate?(this: V): void; - created?(this: V): void; - beforeDestroy?(this: V): void; - destroyed?(this: V): void; - beforeMount?(this: V): void; - mounted?(this: V): void; - beforeUpdate?(this: V): void; - updated?(this: V): void; - activated?(this: V): void; - deactivated?(this: V): void; - - directives?: { [key: string]: DirectiveOptions | DirectiveFunction }; - components?: { [key: string]: Component | AsyncComponent }; + created?(): void; + beforeDestroy?(): void; + destroyed?(): void; + beforeMount?(): void; + mounted?(): void; + beforeUpdate?(): void; + updated?(): void; + activated?(): void; + deactivated?(): void; + + directives?: { [key: string]: DirectiveFunction | DirectiveOptions }; + components?: { [key: string]: Component | AsyncComponent }; transitions?: { [key: string]: Object }; filters?: { [key: string]: Function }; @@ -57,22 +97,23 @@ export interface ComponentOptions { parent?: Vue; mixins?: (ComponentOptions | typeof Vue)[]; name?: string; + // TODO: support properly inferred 'extends' extends?: ComponentOptions | typeof Vue; delimiters?: [string, string]; comments?: boolean; inheritAttrs?: boolean; } -export interface FunctionalComponentOptions { +export interface FunctionalComponentOptions> { name?: string; - props?: string[] | { [key: string]: PropOptions | Constructor | Constructor[] }; + props?: PropDefs; inject?: { [key: string]: string | symbol } | string[]; functional: boolean; - render(this: never, createElement: CreateElement, context: RenderContext): VNode | void; + render(this: undefined, createElement: CreateElement, context: RenderContext): VNode; } -export interface RenderContext { - props: any; +export interface RenderContext { + props: Props; children: VNode[]; slots(): any; data: VNodeData; @@ -80,26 +121,40 @@ export interface RenderContext { injections: any } -export interface PropOptions { - type?: Constructor | Constructor[] | null; +export type Prop = { (): T } | { new (...args: any[]): T & object } + +export type PropValidator = PropOptions | Prop | Prop[]; + +export interface PropOptions { + type?: Prop | Prop[]; required?: boolean; - default?: any; - validator?(value: any): boolean; + default?: T | null | undefined | (() => object); + validator?(value: T): boolean; } -export interface ComputedOptions { - get?(this: V): any; - set?(this: V, value: any): void; +export type RecordPropsDefinition = { + [K in keyof T]: PropValidator +} +export type ArrayPropsDefinition = (keyof T)[]; +export type PropsDefinition = ArrayPropsDefinition | RecordPropsDefinition; + +export interface ComputedOptions { + get?(): T; + set?(value: T): void; cache?: boolean; } -export type WatchHandler = (this: V, val: T, oldVal: T) => void; +export type WatchHandler = (val: T, oldVal: T) => void; export interface WatchOptions { deep?: boolean; immediate?: boolean; } +export interface WatchOptionsWithHandler extends WatchOptions { + handler: WatchHandler; +} + export type DirectiveFunction = ( el: HTMLElement, binding: VNodeDirective, diff --git a/types/test/augmentation-test.ts b/types/test/augmentation-test.ts index dd569052..bb776721 100644 --- a/types/test/augmentation-test.ts +++ b/types/test/augmentation-test.ts @@ -1,4 +1,4 @@ -import Vue = require("../index"); +import Vue from "../index"; declare module "../vue" { // add instance property and method @@ -8,9 +8,9 @@ declare module "../vue" { } // add static property and method - namespace Vue { - const staticProperty: string; - function staticMethod(): void; + interface VueConstructor { + staticProperty: string; + staticMethod(): void; } } @@ -22,10 +22,21 @@ declare module "../options" { } const vm = new Vue({ + props: ["bar"], data: { a: true }, - foo: "foo" + foo: "foo", + methods: { + foo() { + this.a = false; + } + }, + computed: { + BAR(): string { + return this.bar.toUpperCase(); + } + } }); vm.$instanceProperty; diff --git a/types/test/options-test.ts b/types/test/options-test.ts index cbd5be14..6a94d95c 100644 --- a/types/test/options-test.ts +++ b/types/test/options-test.ts @@ -1,14 +1,76 @@ -import Vue = require("../index"); +import Vue from "../index"; import { AsyncComponent, ComponentOptions, FunctionalComponentOptions } from "../index"; +import { CreateElement } from "../vue"; interface Component extends Vue { a: number; } +Vue.component('sub-component', { + components: { + a: Vue.component(""), + b: {} + } +}); + +Vue.component('prop-component', { + props: { + size: Number, + name: { + type: String, + default: '0', + required: true, + } + }, + data() { + return { + fixedSize: this.size.toFixed(), + capName: this.name.toUpperCase() + } + } +}); + +Vue.component('string-prop', { + props: ['size', 'name'], + data() { + return { + fixedSize: this.size.whatever, + capName: this.name.isany + } + } +}); + +class User { + private u: number +} +class Cat { + private u: number +} + +Vue.component('union-prop', { + props: { + primitive: [String, Number], + object: [Cat, User], + regex: RegExp, + mixed: [RegExp, Array], + union: [User, Number] as {new(): User | Number}[] // requires annotation + }, + data() { + this.primitive; + this.object; + this.union; + this.regex.compile; + this.mixed; + return { + fixedSize: this.union, + } + } +}); + Vue.component('component', { data() { this.$mount - this.a + this.size return { a: 1 } @@ -17,25 +79,22 @@ Vue.component('component', { size: Number, name: { type: String, - default: 0, + default: '0', required: true, - validator(value) { - return value > 0; - } } }, propsData: { msg: "Hello" }, computed: { - aDouble(this: Component) { + aDouble(): number { return this.a * 2; }, aPlus: { - get(this: Component) { + get(): number { return this.a + 1; }, - set(this: Component, v: number) { + set(v: number) { this.a = v - 1; }, cache: false @@ -44,6 +103,9 @@ Vue.component('component', { methods: { plus() { this.a++; + this.aDouble.toFixed(); + this.aPlus = 1; + this.size.toFixed(); } }, watch: { @@ -92,15 +154,15 @@ Vue.component('component', { createElement("div", "message"), createElement(Vue.component("component")), createElement({} as ComponentOptions), - createElement({ functional: true, render () {}}), + createElement({ + functional: true, + render(c: CreateElement) { + return createElement() + } + }), createElement(() => Vue.component("component")), createElement(() => ( {} as ComponentOptions )), - createElement(() => { - return new Promise((resolve) => { - resolve({} as ComponentOptions); - }) - }), createElement((resolve, reject) => { resolve({} as ComponentOptions); reject(); @@ -114,7 +176,7 @@ Vue.component('component', { staticRenderFns: [], beforeCreate() { - this.a = 1; + (this as any).a = 1; }, created() {}, beforeDestroy() {}, @@ -160,7 +222,7 @@ Vue.component('component', { name: "Component", extends: {} as ComponentOptions, delimiters: ["${", "}"] -} as ComponentOptions); +}); Vue.component('component-with-scoped-slot', { render (h) { @@ -183,15 +245,15 @@ Vue.component('component-with-scoped-slot', { }, components: { child: { - render (h) { + render (this: Vue, h: CreateElement) { return h('div', [ this.$scopedSlots['default']({ msg: 'hi' }), this.$scopedSlots['item']({ msg: 'hello' }) ]) } - } as ComponentOptions + } } -} as ComponentOptions) +}) Vue.component('functional-component', { props: ['prop'], @@ -205,13 +267,16 @@ Vue.component('functional-component', { context.parent; return createElement("div", {}, context.children); } -} as FunctionalComponentOptions); +}); Vue.component('functional-component-object-inject', { functional: true, inject: { foo: 'bar', baz: Symbol() + }, + render(h) { + return h('div') } }) @@ -220,8 +285,8 @@ Vue.component("async-component", ((resolve, reject) => { resolve(Vue.component("component")); }, 0); return new Promise((resolve) => { - resolve({ functional: true } as FunctionalComponentOptions); + resolve({ functional: true }); }) -}) as AsyncComponent); +})); -Vue.component('async-es-module-component', (() => import('./es-module')) as AsyncComponent) +Vue.component('async-es-module-component', () => import('./es-module')) diff --git a/types/test/plugin-test.ts b/types/test/plugin-test.ts index 211f248a..15055614 100644 --- a/types/test/plugin-test.ts +++ b/types/test/plugin-test.ts @@ -1,4 +1,4 @@ -import Vue = require("../index"); +import Vue from "../index"; import { PluginFunction, PluginObject } from "../index"; class Option { diff --git a/types/test/tsconfig.json b/types/test/tsconfig.json index cbc565a7..be0fef22 100644 --- a/types/test/tsconfig.json +++ b/types/test/tsconfig.json @@ -6,8 +6,7 @@ "es2015" ], "module": "commonjs", - "noImplicitAny": true, - "strictNullChecks": true, + "strict": true, "noEmit": true }, "files": [ diff --git a/types/test/vue-test.ts b/types/test/vue-test.ts index 50605998..e497c602 100644 --- a/types/test/vue-test.ts +++ b/types/test/vue-test.ts @@ -1,4 +1,5 @@ -import Vue = require("../index"); +import Vue, { VNode } from "../index"; +import { ComponentOptions } from "../options"; class Test extends Vue { a: number; @@ -90,9 +91,91 @@ class Test extends Vue { this.directive("", {bind() {}}); this.filter("", (value: number) => value); this.component("", { data: () => ({}) }); - this.component("", { functional: true, render () {}}); + this.component("", { functional: true, render(h) { return h("div", "hello!") } }); this.use; this.mixin(Test); this.compile("
{{ message }}
"); } } + +const HelloWorldComponent = Vue.extend({ + props: ["name"], + data() { + return { + message: "Hello " + this.name, + } + }, + computed: { + shouted(): string { + return this.message.toUpperCase(); + } + }, + methods: { + getMoreExcited() { + this.message += "!"; + } + }, + watch: { + message(a: string) { + console.log(`Message ${this.message} was changed!`); + } + } +}); + +const FunctionalHelloWorldComponent = Vue.extend({ + functional: true, + props: ["name"], + render(createElement, ctxt) { + return createElement("div", "Hello " + ctxt.props.name) + } +}); + +const Parent = Vue.extend({ + data() { + return { greeting: 'Hello' } + } +}); + +const Child = Parent.extend({ + methods: { + foo() { + console.log(this.greeting.toLowerCase()); + } + } +}); + +const GrandChild = Child.extend({ + computed: { + lower(): string { + return this.greeting.toLowerCase(); + } + } +}); + +new GrandChild().lower.toUpperCase(); +for (let _ in (new Test()).$options) { +} +declare const options: ComponentOptions; +Vue.extend(options); +Vue.component('test-comp', options); +new Vue(options); + +// cyclic example +Vue.extend({ + props: { + bar: { + type: String + } + }, + methods: { + foo() {} + }, + mounted () { + this.foo() + }, + // manual annotation + render (h): VNode { + const a = this.bar + return h('canvas', {}, [a]) + } +}) diff --git a/types/tsconfig.json b/types/tsconfig.json new file mode 100644 index 00000000..dc2f0455 --- /dev/null +++ b/types/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "lib": [ + "es2015", "dom" + ] + }, + "include": [ + "./*.ts" + ] +} \ No newline at end of file diff --git a/types/vue.d.ts b/types/vue.d.ts index 338a9ad2..67c5bd04 100644 --- a/types/vue.d.ts +++ b/types/vue.d.ts @@ -3,50 +3,39 @@ import { AsyncComponent, ComponentOptions, FunctionalComponentOptions, - WatchOptions, + WatchOptionsWithHandler, WatchHandler, DirectiveOptions, - DirectiveFunction + DirectiveFunction, + RecordPropsDefinition, + ThisTypedComponentOptionsWithArrayProps, + ThisTypedComponentOptionsWithRecordProps, + WatchOptions, } from "./options"; import { VNode, VNodeData, VNodeChildren, ScopedSlot } from "./vnode"; import { PluginFunction, PluginObject } from "./plugin"; -export type CreateElement = { - // empty node - (): VNode; - - // element or component name - (tag: string, children: VNodeChildren): VNode; - (tag: string, data?: VNodeData, children?: VNodeChildren): VNode; - - // component constructor or options - (tag: Component, children: VNodeChildren): VNode; - (tag: Component, data?: VNodeData, children?: VNodeChildren): VNode; - - // async component - (tag: AsyncComponent, children: VNodeChildren): VNode; - (tag: AsyncComponent, data?: VNodeData, children?: VNodeChildren): VNode; +export interface CreateElement { + (tag?: string | Component | AsyncComponent, children?: VNodeChildren): VNode; + (tag?: string | Component | AsyncComponent, data?: VNodeData, children?: VNodeChildren): VNode; } -export declare class Vue { - - constructor(options?: ComponentOptions); - - $data: Object; +export interface Vue { readonly $el: HTMLElement; readonly $options: ComponentOptions; readonly $parent: Vue; readonly $root: Vue; readonly $children: Vue[]; - readonly $refs: { [key: string]: Vue | Element | Vue[] | Element[]}; + readonly $refs: { [key: string]: Vue | Element | Vue[] | Element[] }; readonly $slots: { [key: string]: VNode[] }; readonly $scopedSlots: { [key: string]: ScopedSlot }; readonly $isServer: boolean; + readonly $data: Record; + readonly $props: Record; readonly $ssrContext: any; - readonly $props: any; readonly $vnode: VNode; - readonly $attrs: { [key: string] : string }; - readonly $listeners: { [key: string]: Function | Array }; + readonly $attrs: Record; + readonly $listeners: Record; $mount(elementOrSelector?: Element | String, hydrating?: boolean): this; $forceUpdate(): void; @@ -55,12 +44,12 @@ export declare class Vue { $delete: typeof Vue.delete; $watch( expOrFn: string, - callback: WatchHandler, + callback: (this: this, n: any, o: any) => void, options?: WatchOptions ): (() => void); $watch( expOrFn: (this: this) => T, - callback: WatchHandler, + callback: (this: this, n: T, o: T) => void, options?: WatchOptions ): (() => void); $on(event: string | string[], callback: Function): this; @@ -70,8 +59,54 @@ export declare class Vue { $nextTick(callback: (this: this) => void): void; $nextTick(): Promise; $createElement: CreateElement; +} + +export type CombinedVueInstance = Instance & Data & Methods & Computed & Props; +export type ExtendedVue = VueConstructor & Vue>; + +export interface VueConstructor { + new (options?: ThisTypedComponentOptionsWithArrayProps): CombinedVueInstance>; + // ideally, the return type should just contains Props, not Record. But TS requires Base constructors must all have the same return type. + new (options?: ThisTypedComponentOptionsWithRecordProps): CombinedVueInstance>; + new (options?: ComponentOptions): CombinedVueInstance>; + + extend(definition: FunctionalComponentOptions, PropNames[]>): ExtendedVue>; + extend(definition: FunctionalComponentOptions>): ExtendedVue; + extend(options?: ThisTypedComponentOptionsWithArrayProps): ExtendedVue>; + extend(options?: ThisTypedComponentOptionsWithRecordProps): ExtendedVue; + extend(options?: ComponentOptions): ExtendedVue; + + nextTick(callback: () => void, context?: any[]): void; + nextTick(): Promise + set(object: Object, key: string, value: T): T; + set(array: T[], key: number, value: T): T; + delete(object: Object, key: string): void; + delete(array: T[], key: number): void; + + directive( + id: string, + definition?: DirectiveOptions | DirectiveFunction + ): DirectiveOptions; + filter(id: string, definition?: Function): Function; - static config: { + component(id: string): VueConstructor; + component(id: string, constructor: VC): VC; + component(id: string, definition: AsyncComponent): ExtendedVue; + component(id: string, definition: FunctionalComponentOptions, PropNames[]>): ExtendedVue>; + component(id: string, definition: FunctionalComponentOptions>): ExtendedVue; + component(id: string, definition?: ThisTypedComponentOptionsWithArrayProps): ExtendedVue>; + component(id: string, definition?: ThisTypedComponentOptionsWithRecordProps): ExtendedVue; + component(id: string, definition?: ComponentOptions): ExtendedVue; + + use(plugin: PluginObject | PluginFunction, options?: T): void; + use(plugin: PluginObject | PluginFunction, ...options: any[]): void; + mixin(mixin: VueConstructor | ComponentOptions): void; + compile(template: string): { + render(createElement: typeof Vue.prototype.$createElement): VNode; + staticRenderFns: (() => VNode)[]; + }; + + config: { silent: boolean; optionMergeStrategies: any; devtools: boolean; @@ -82,27 +117,6 @@ export declare class Vue { ignoredElements: string[]; keyCodes: { [key: string]: number | number[] }; } - - static extend(options: ComponentOptions | FunctionalComponentOptions): typeof Vue; - static nextTick(callback: () => void, context?: any[]): void; - static nextTick(): Promise - static set(object: Object, key: string, value: T): T; - static set(array: T[], key: number, value: T): T; - static delete(object: Object, key: string): void; - static delete(array: T[], key: number): void; - - static directive( - id: string, - definition?: DirectiveOptions | DirectiveFunction - ): DirectiveOptions; - static filter(id: string, definition?: Function): Function; - static component(id: string, definition?: Component | AsyncComponent): typeof Vue; - - static use(plugin: PluginObject | PluginFunction, options?: T): void; - static use(plugin: PluginObject | PluginFunction, ...options: any[]): void; - static mixin(mixin: typeof Vue | ComponentOptions): void; - static compile(template: string): { - render(createElement: typeof Vue.prototype.$createElement): VNode; - staticRenderFns: (() => VNode)[]; - }; } + +export const Vue: VueConstructor;