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
base: dev
from
Commits
Jump to file or symbol
Failed to load files and symbols.
+288 −139
Diff settings

Always

Just for now

Copy path View file
@@ -112,7 +112,7 @@
"rollup-watch": "^4.0.0",
"selenium-server": "^2.53.1",
"serialize-javascript": "^1.3.0",
"typescript": "^2.3.4",
"typescript": "2.5.0-dev.20170615",

This comment has been minimized.

@znck

znck Aug 2, 2017

Member

Shouldn't it be depending on a stable version?

This comment has been minimized.

@nickmessing

nickmessing Aug 2, 2017

Member

It depended on some experimental changes but afaik they landed in 2.4, didn't they @DanielRosenwasser?

"uglify-js": "^3.0.15",
"webpack": "^2.6.1",
"weex-js-runtime": "^0.20.5",
Copy path View file
@@ -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";
Copy path View file
@@ -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<Instance extends Vue, Data, Methods, Computed, PropNames extends string> =

This comment has been minimized.

@HerringtonDarkholme

HerringtonDarkholme Jun 16, 2017

Member

Can we re-export ThisTyped option in index? It is useful for library authors, I think.

This comment has been minimized.

@DanielRosenwasser

DanielRosenwasser Jun 16, 2017

Is that a great idea? Does re-exporting mean that this is committed to as part of the public interface?

This comment has been minimized.

@DanielRosenwasser

DanielRosenwasser Jun 17, 2017

Also, should I just rename this to ComponentOptionsWithArrayProps?

object &
ComponentOptions<Data | ((this: Record<PropNames, any> & Instance) => Data), Methods, Computed, PropNames[]> &

This comment has been minimized.

@HerringtonDarkholme

HerringtonDarkholme Jun 16, 2017

Member

👍 for only exposing prop!

This comment has been minimized.

@DanielRosenwasser

DanielRosenwasser Jun 16, 2017

I agree! To be honest this was more a limitation of the type system but it honestly seemed like more of a feature than a bug. 😄

ThisType<CombinedVueInstance<Instance, 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<Instance extends Vue, Data, Methods, Computed, Props> =
object &
ComponentOptions<Data | ((this: Record<keyof Props, any> & Instance) => Data), Methods, Computed, Props> &
ThisType<CombinedVueInstance<Instance, Data, Methods, Computed, Props>>;

This comment has been minimized.

@HerringtonDarkholme

HerringtonDarkholme Jul 10, 2017

Member

Here Props should be Record<keyof Props, any>, right?


/**
* 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<Vue, Data, Methods, Computed, PropNames>
| ThisTypedComponentOptionsWithRecordProps<Vue, 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 };

This comment has been minimized.

@HerringtonDarkholme

HerringtonDarkholme Jun 15, 2017

Member

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?

This comment has been minimized.

@DanielRosenwasser

DanielRosenwasser Jun 15, 2017

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!

This comment has been minimized.

@HerringtonDarkholme

HerringtonDarkholme Jun 15, 2017

Member

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 };

@@ -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[];

This comment has been minimized.

@HerringtonDarkholme

HerringtonDarkholme Jun 16, 2017

Member

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

This comment has been minimized.

@DanielRosenwasser

DanielRosenwasser Jun 16, 2017

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,
Copy path View file
@@ -1,4 +1,4 @@
import Vue = require("../index");
import Vue from "../index";

declare module "../vue" {
// add instance property and method
@@ -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;
Copy path View file
@@ -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;
@@ -19,7 +20,7 @@ Vue.component('component', {
type: String,
default: 0,
required: true,
validator(value) {
validator(value: number) {
return value > 0;
}
}
@@ -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();
}),

@@ -147,7 +153,7 @@ Vue.component('component', {
},
components: {
a: Vue.component(""),
b: {} as ComponentOptions<Vue>
b: {} as ComponentOptions<object, object, object, object>
},
transitions: {},
filters: {
@@ -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) {
@@ -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'],
@@ -204,7 +210,7 @@ Vue.component('functional-component', {
context.parent;
return createElement("div", {}, context.children);
}
} as FunctionalComponentOptions);
});

Vue.component("async-component", (resolve, reject) => {
setTimeout(() => {
Oops, something went wrong.
ProTip! Use n and p to navigate between commits in a pull request.