Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Vue type declarations for more canonical usage #5887

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5b5a88f
refactor TypeScript typings to use ES style exports
yyx990803 Feb 26, 2017
385a744
Allow functions in 'methods' & 'computed' to view themselves, as well…
DanielRosenwasser Mar 8, 2017
8cd5b9c
Got 'new Vue(...)', 'Vue.extend(...)', and 'Vue.component(...)' working.
DanielRosenwasser Mar 16, 2017
540a38f
Made it so that any 'data' function can only access 'props' and base …
DanielRosenwasser Mar 27, 2017
f34f4f6
Improved defaults, fixed overloads and types for functional component…
DanielRosenwasser Jun 1, 2017
b1f40ce
Condensed declaration of 'watch'.
DanielRosenwasser Jun 1, 2017
355ff75
Added two tests for 'extend'.
DanielRosenwasser Jun 1, 2017
bc54007
.\types\options.d.ts
DanielRosenwasser Jun 1, 2017
e7ea5bb
Updated tests, tighted strictness.
DanielRosenwasser Jun 1, 2017
ebde0b1
Merge remote-tracking branch 'upstream/dev' into accurateVueTypes
DanielRosenwasser Jun 1, 2017
d78d14b
Made the Vue instance non-generic, made readonly, augmented tests.
DanielRosenwasser Jun 14, 2017
fc83771
Make it possible to extend Vue without type arguments.
DanielRosenwasser Jun 14, 2017
a50c838
Removed 'ThisTypedComponentOptions'.
DanielRosenwasser Jun 14, 2017
3c86b10
Merge remote-tracking branch 'upstream/dev' into accurateVueTypes
DanielRosenwasser Jun 14, 2017
33a106c
Upgraded dependency on TypeScript.
DanielRosenwasser Jun 15, 2017
0f586db
Added test by @ktsn.
DanielRosenwasser Jun 15, 2017
1092efe
Removed unnecessary mixin constructors, made 'VueConstructor' generic.
DanielRosenwasser Jun 15, 2017
ebd8c0b
Merge remote-tracking branch 'upstream/dev' into accurateVueTypes
DanielRosenwasser Jun 23, 2017
c628103
[release] weex-vue-framework@2.4.2-weex.1 (#6196)
Hanks10100 Jul 24, 2017
e4a8545
Merge remote-tracking branch 'upstream/dev' into accurateVueTypes
DanielRosenwasser Jul 28, 2017
f7ebfa3
Props -> Record<keyof Props, any>
DanielRosenwasser Aug 16, 2017
bb0ff30
Update TypeScript devDependency.
DanielRosenwasser Aug 16, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 31 additions & 32 deletions types/index.d.ts
Original file line number Diff line number Diff line change
@@ -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<V extends Vue> = Options.ComponentOptions<V>;
export type FunctionalComponentOptions = Options.FunctionalComponentOptions;
export type RenderContext = Options.RenderContext;
export type PropOptions = Options.PropOptions;
export type ComputedOptions<V extends Vue> = Options.ComputedOptions<V>;
export type WatchHandler<V extends Vue> = Options.WatchHandler<V, any>;
export type WatchOptions = Options.WatchOptions;
export type DirectiveFunction = Options.DirectiveFunction;
export type DirectiveOptions = Options.DirectiveOptions;
export {
CreateElement
} from "./vue";

export type PluginFunction<T> = Plugin.PluginFunction<T>;
export type PluginObject<T> = Plugin.PluginObject<T>;
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";
120 changes: 83 additions & 37 deletions types/options.d.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,82 @@
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<Vue> | FunctionalComponentOptions;
export type AsyncComponent = (
resolve: (component: Component) => void,
export type Component<Data, Methods, Computed, PropNames extends string = never> =
typeof Vue |
FunctionalOrStandardComponentOptions<Data, Methods, Computed, PropNames>;

export type AsyncComponent<Data, Methods, Computed, PropNames extends string> = (
resolve: (component: Component<Data, Methods, Computed, PropNames>) => void,
reject: (reason?: any) => void
) => Promise<Component> | Component | void;
) => Promise<Component<Data, Methods, Computed, PropNames>> | Component<Data, Methods, Computed, PropNames> | 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<Computed>` and work backwards.
*/
export type Accessors<T> = {
[K in keyof T]: (() => T[K]) | ComputedOptions<T[K]>
}

export interface ComponentOptions<V extends Vue> {
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<Data, Methods, Computed, PropNames extends string> =
object &
ComponentOptions<Data | ((this: Record<PropNames, any> & Vue) => Data), Methods, Computed, PropNames[]> &
ThisType<CombinedVueInstance<Data, Methods, Computed, Record<PropNames, any>>>;

/**
* This type should be used when an object mapped to `PropOptions` is used for a component's `props` value.
*/
export type ThisTypedComponentOptionsWithRecordProps<Data, Methods, Computed, Props> =
object &
ComponentOptions<Data | ((this: Record<keyof Props, any> & Vue) => Data), Methods, Computed, Props> &
ThisType<CombinedVueInstance<Data, Methods, Computed, Props>>;

/**
* A helper type that describes options for either functional or non-functional components.
* Useful for `Vue.extend` and `Vue.component`.
*/
export type FunctionalOrStandardComponentOptions<Data, Methods, Computed, PropNames extends string = never> =
| FunctionalComponentOptions<PropNames[] | Record<PropNames, PropValidator>, Record<PropNames, any>>
| ThisTypedComponentOptionsWithArrayProps<Data, Methods, Computed, PropNames>
| ThisTypedComponentOptionsWithRecordProps<Data, Methods, Computed, Record<PropNames, PropOptions>>;


export interface ComponentOptions<Data, Methods, Computed, Props> {
data?: Data;
props?: Props;
propsData?: Object;
computed?: { [key: string]: ((this: V) => any) | ComputedOptions<V> };
methods?: { [key: string]: (this: V, ...args: any[]) => any };
Copy link
Member

@HerringtonDarkholme HerringtonDarkholme Jun 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might also need methods?: ThisType<Methods & Computed & Pops & Data> to fully capture Vue's this instance.

I haven't try it myself, but will it cause cyclic reference?

Copy link
Author

@DanielRosenwasser DanielRosenwasser Jun 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't try it myself, but will it cause cyclic reference?

There are a few problems that potentially come up if you try to reference methods from data in which you'll have to have an explicit annotation on data(). But we can be prescriptive and tell people to just use functions when you need to operate on data in some way.

We might also need methods?: ThisType<Methods & Computed & Pops & Data> to fully capture Vue's this instance.

@HerringtonDarkholme that's not actually necessary. When a method needs its this type, it will walk up each contextually typed object literal until it finds one that had a contextual type consisting of ThisType. If it does, it uses it. If not, it uses the innermost object literal's contextual type, and if there isn't one it just uses the type of the containing literal.

You should definitely try it out!

Copy link
Member

@HerringtonDarkholme HerringtonDarkholme Jun 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried it locally. It works like charm!

Sorry for the wrong reporting. I only tried accurateVueType in vetur, which seems to wrongly configured. I will investigate more in vetur.

Vetur's issue is caused by ComponentOption's type arg.

watch?: { [key: string]: ({ handler: WatchHandler<V, any> } & WatchOptions) | WatchHandler<V, any> | string };
computed?: Accessors<Computed>;
methods?: Methods;
watch?: Record<string, WatchOptionsWithHandler<any> | WatchHandler<any> | 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 };
beforeCreate?(): void;
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<any, any, any, never> | AsyncComponent<any, any, any, never> };
transitions?: { [key: string]: Object };
filters?: { [key: string]: Function };

Expand All @@ -50,48 +89,55 @@ export interface ComponentOptions<V extends Vue> {
};

parent?: Vue;
mixins?: (ComponentOptions<Vue> | typeof Vue)[];
mixins?: (ComponentOptions<any, any, any, any> | typeof Vue)[];
name?: string;
extends?: ComponentOptions<Vue> | typeof Vue;
// TODO: support properly inferred 'extends'
extends?: ComponentOptions<any, any, any, any> | typeof Vue;
delimiters?: [string, string];
}

export interface FunctionalComponentOptions {
export interface FunctionalComponentOptions<Props = object, ContextProps = object> {
name?: string;
props?: string[] | { [key: string]: PropOptions | Constructor | Constructor[] };
props?: Props;
functional: boolean;
render(this: never, createElement: CreateElement, context: RenderContext): VNode | void;
render(this: undefined, createElement: CreateElement, context: RenderContext<ContextProps>): VNode;
}

export interface RenderContext {
props: any;
export interface RenderContext<Props> {
props: Props;
children: VNode[];
slots(): any;
data: VNodeData;
parent: Vue;
injections: any
}

export type PropValidator = PropOptions | Constructor | Constructor[];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any chance to infer Prop type from Constructor? Something like the reverse of emitDecoratorMetadata

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally tried to do that but the more I thought about it, the more time I realized it'd take to finish. I figured it'd be better to put the PR out and try that at a later stage.


export interface PropOptions {
type?: Constructor | Constructor[] | null;
required?: boolean;
default?: any;
default?: string | number | boolean | null | undefined | (() => object);
validator?(value: any): boolean;
}

export interface ComputedOptions<V> {
get?(this: V): any;
set?(this: V, value: any): void;
export interface ComputedOptions<T> {
get?(): T;
set?(value: T): void;
cache?: boolean;
}

export type WatchHandler<V, T> = (this: V, val: T, oldVal: T) => void;
export type WatchHandler<T> = (val: T, oldVal: T) => void;

export interface WatchOptions {
deep?: boolean;
immediate?: boolean;
}

export interface WatchOptionsWithHandler<T> extends WatchOptions {
handler: WatchHandler<T>;
}

export type DirectiveFunction = (
el: HTMLElement,
binding: VNodeDirective,
Expand Down
22 changes: 16 additions & 6 deletions types/test/augmentation-test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Vue = require("../index");
import Vue from "../index";

declare module "../vue" {
// add instance property and method
Expand All @@ -8,24 +8,34 @@ declare module "../vue" {
}

// add static property and method
namespace Vue {
const staticProperty: string;
function staticMethod(): void;
interface VueConstructor {
staticProperty: string;
staticMethod(): void;
}
}

// augment ComponentOptions
declare module "../options" {
interface ComponentOptions<V extends Vue> {
interface ComponentOptions<Data, Methods, Computed, Props> {
foo?: string;
}
}

const vm = new Vue({
props: ["bar"],
data: {
a: true
},
foo: "foo"
methods: {
foo() {
this.a = false;
}
},
computed: {
BAR(): string {
return this.bar.toUpperCase();
}
}
});

vm.$instanceProperty;
Expand Down
36 changes: 21 additions & 15 deletions types/test/options-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Vue = require("../index");
import Vue from "../index";
import { ComponentOptions, FunctionalComponentOptions } from "../index";
import { CreateElement } from "../vue";

interface Component extends Vue {
a: number;
Expand All @@ -19,7 +20,7 @@ Vue.component('component', {
type: String,
default: 0,
required: true,
validator(value) {
validator(value: number) {
return value > 0;
}
}
Expand Down Expand Up @@ -91,18 +92,23 @@ Vue.component('component', {
createElement(),
createElement("div", "message"),
createElement(Vue.component("component")),
createElement({} as ComponentOptions<Vue>),
createElement({ functional: true, render () {}}),
createElement({} as ComponentOptions<object, object, object, object>),
createElement({
functional: true,
render(c: CreateElement) {
return createElement()
}
}),

createElement(() => Vue.component("component")),
createElement(() => ( {} as ComponentOptions<Vue> )),
createElement(() => ( {} as ComponentOptions<object, object, object, object> )),
createElement(() => {
return new Promise((resolve) => {
resolve({} as ComponentOptions<Vue>);
resolve({} as ComponentOptions<object, object, object, object>);
})
}),
createElement((resolve, reject) => {
resolve({} as ComponentOptions<Vue>);
resolve({} as ComponentOptions<object, object, object, object>);
reject();
}),

Expand Down Expand Up @@ -147,7 +153,7 @@ Vue.component('component', {
},
components: {
a: Vue.component(""),
b: {} as ComponentOptions<Vue>
b: {} as ComponentOptions<object, object, object, object>
},
transitions: {},
filters: {
Expand All @@ -156,11 +162,11 @@ Vue.component('component', {
}
},
parent: new Vue,
mixins: [Vue.component(""), ({} as ComponentOptions<Vue>)],
mixins: [Vue.component(""), ({} as ComponentOptions<object, object, object, object>)],
name: "Component",
extends: {} as ComponentOptions<Vue>,
extends: {} as ComponentOptions<object, object, object, object>,
delimiters: ["${", "}"]
} as ComponentOptions<Component>);
});

Vue.component('component-with-scoped-slot', {
render (h) {
Expand All @@ -183,15 +189,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<Vue>
}
}
} as ComponentOptions<Vue>)
})

Vue.component('functional-component', {
props: ['prop'],
Expand All @@ -204,7 +210,7 @@ Vue.component('functional-component', {
context.parent;
return createElement("div", {}, context.children);
}
} as FunctionalComponentOptions);
});

Vue.component("async-component", (resolve, reject) => {
setTimeout(() => {
Expand Down
2 changes: 1 addition & 1 deletion types/test/plugin-test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Vue = require("../index");
import Vue from "../index";
import { PluginFunction, PluginObject } from "../index";

class Option {
Expand Down
Loading