Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 7c87b03

Browse filesBrowse files
committed
fix: correctly type mixin type when used with props and emits helpers
1 parent a475e97 commit 7c87b03
Copy full SHA for 7c87b03

File tree

Expand file treeCollapse file tree

7 files changed

+199
-12
lines changed
Filter options
Expand file treeCollapse file tree

7 files changed

+199
-12
lines changed

‎package.json

Copy file name to clipboardExpand all lines: package.json
+2-1Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@
2121
"dev": "webpack --config example/webpack.config.js --watch",
2222
"lint": "prettier --parser typescript \"**/*.[jt]s?(x)\"",
2323
"lint:fix": "yarn lint --write",
24-
"test": "yarn test:ts && yarn test:babel",
24+
"test": "yarn test:ts && yarn test:babel && yarn test:dts",
2525
"test:ts": "jest",
2626
"test:babel": "BABEL_TEST=1 jest",
27+
"test:dts": "tsc -p ./test-dts",
2728
"docs:dev": "vuepress dev docs",
2829
"docs:build": "vuepress build docs",
2930
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",

‎src/helpers.ts

Copy file name to clipboardExpand all lines: src/helpers.ts
+17-8Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ComponentOptions, UnwrapRef, ComponentObjectPropsOptions, ExtractPropTypes } from 'vue'
2-
import { EmitsOptions, ObjectEmitsOptions, Vue, VueConstructor, VueMixin } from './vue'
2+
import { ClassComponentHooks, EmitsOptions, ObjectEmitsOptions, Vue, VueBase, VueConstructor, VueMixin } from './vue'
33

44
export function Options<V extends Vue>(
55
options: ComponentOptions & ThisType<V>
@@ -37,12 +37,6 @@ export function createDecorator(
3737
}
3838
}
3939

40-
export interface PropsMixin {
41-
new <Props = unknown>(...args: any[]): {
42-
$props: Props
43-
}
44-
}
45-
4640
export type UnionToIntersection<U> = (
4741
U extends any ? (k: U) => void : never
4842
) extends (k: infer I) => void
@@ -51,8 +45,23 @@ export type UnionToIntersection<U> = (
5145

5246
export type ExtractInstance<T> = T extends VueMixin<infer V> ? V : never
5347

48+
export type NarrowEmit<T extends VueBase>
49+
= Omit<T, '$emit' | keyof ClassComponentHooks>
50+
51+
// Reassign class component hooks as mapped types makes prototype function (`mounted(): void`) instance function (`mounted: () => void`).
52+
& ClassComponentHooks
53+
54+
// Exclude generic $emit type (`$emit: (event: string, ...args: any[]) => void`) if there are another intersected type.
55+
& {
56+
$emit: T['$emit'] extends ((event: string, ...args: any[]) => void) & infer R
57+
? unknown extends R
58+
? T['$emit']
59+
: R
60+
: T['$emit']
61+
}
62+
5463
export type MixedVueBase<Mixins extends VueMixin[]> = Mixins extends (infer T)[]
55-
? VueConstructor<UnionToIntersection<ExtractInstance<T>> & Vue> & PropsMixin
64+
? VueConstructor<NarrowEmit<UnionToIntersection<ExtractInstance<T>> & Vue> & VueBase>
5665
: never
5766

5867
export function mixins<T extends VueMixin[]>(...Ctors: T): MixedVueBase<T>

‎src/index.ts

Copy file name to clipboardExpand all lines: src/index.ts
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ export {
1717
MixedVueBase,
1818
UnionToIntersection,
1919
ExtractInstance,
20-
PropsMixin,
20+
NarrowEmit
2121
} from './helpers'

‎src/vue.ts

Copy file name to clipboardExpand all lines: src/vue.ts
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export interface VueStatic {
8686

8787
export type VueBase = Vue<unknown, never[]>
8888

89-
export type VueMixin<V extends VueBase = Vue> = VueStatic & { prototype: V }
89+
export type VueMixin<V extends VueBase = VueBase> = VueStatic & { prototype: V }
9090

9191
export interface ClassComponentHooks {
9292
// To be extended on user land
@@ -121,7 +121,7 @@ export type Vue<Props = unknown, Emits extends EmitsOptions = {}> = ComponentPub
121121
> &
122122
ClassComponentHooks
123123

124-
export interface VueConstructor<V extends VueBase = Vue> extends VueStatic {
124+
export interface VueConstructor<V extends VueBase = Vue> extends VueMixin<V> {
125125
new (...args: any[]): V
126126
}
127127

‎test-dts/helpers.d.ts

Copy file name to clipboard
+9Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
declare function describe(name: string, fn: () => void): void
2+
declare function it(name: string, fn: () => void): void
3+
4+
declare function equals<T, U>(value: Equals<T, U>): void
5+
6+
// https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-421529650
7+
type Equals<X, Y> =
8+
(<T>() => T extends X ? 1 : 2) extends
9+
(<T>() => T extends Y ? 1 : 2) ? true : false;

‎test-dts/mixins.ts

Copy file name to clipboard
+156Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { Vue, props, emits, mixins } from '../src'
2+
3+
describe('mixins', () => {
4+
it('mixes multiple Vue class', () => {
5+
class Foo extends Vue {
6+
foo: string = ''
7+
}
8+
9+
class Bar extends Vue {
10+
bar: number = 0
11+
}
12+
13+
class Baz extends Vue {
14+
baz: boolean = false
15+
}
16+
17+
class App extends mixins(Foo, Bar, Baz) {
18+
mounted() {
19+
const vm = this
20+
equals<typeof vm.foo, string>(true)
21+
equals<typeof vm.bar, number>(true)
22+
equals<typeof vm.baz, boolean>(true)
23+
// @ts-expect-error
24+
this.nonExist
25+
26+
equals<typeof vm.$emit, ((event: string, ...args: any[]) => void) & ((event: never, ...args: any[]) => void)>(true)
27+
}
28+
}
29+
})
30+
31+
it('mixes props and emits mixins', () => {
32+
const Props = props({
33+
value: String
34+
})
35+
36+
const Emits = emits({
37+
input: (value: string) => true
38+
})
39+
40+
class App extends mixins(Props, Emits) {
41+
mounted() {
42+
const vm = this
43+
equals<typeof vm.$props.value, string | undefined>(true)
44+
equals<typeof vm.$emit, ((event: 'input', value: string) => void) & ((event: never, ...args: any[]) => void)>(true)
45+
}
46+
}
47+
})
48+
})
49+
50+
describe('props', () => {
51+
it('types with array style props definition', () => {
52+
const Props = props(['foo'])
53+
class App extends Props {
54+
mounted() {
55+
const vm = this
56+
equals<typeof vm.foo, any>(true)
57+
equals<typeof vm.$props.foo, any>(true)
58+
59+
// @ts-expect-error
60+
this.bar
61+
// @ts-expect-error
62+
this.$props.bar
63+
}
64+
}
65+
})
66+
67+
it('types with object style props definition', () => {
68+
const Props = props({
69+
foo: {
70+
type: Number,
71+
default: 42
72+
},
73+
74+
bar: {
75+
type: String,
76+
required: true
77+
},
78+
79+
baz: {
80+
type: Boolean
81+
}
82+
})
83+
84+
class App extends Props {
85+
mounted() {
86+
type ExpectedProps = Readonly<{
87+
foo: number
88+
bar: string
89+
} & {
90+
baz?: boolean | undefined
91+
}>
92+
93+
const vm = this
94+
equals<typeof vm.foo, number>(true)
95+
equals<typeof vm.bar, string>(true)
96+
equals<typeof vm.baz, boolean | undefined>(true)
97+
equals<typeof vm.$props, ExpectedProps>(true)
98+
99+
// @ts-expect-error
100+
this.nonExist
101+
}
102+
}
103+
})
104+
105+
it('does not lose $emit type', () => {
106+
const Props = props(['foo'])
107+
class App extends Props {
108+
mounted() {
109+
const vm = this
110+
equals<typeof vm.$emit, (event: string, ...args: any[]) => void>(true)
111+
equals<typeof vm.$emit, any>(false)
112+
}
113+
}
114+
})
115+
})
116+
117+
describe('emits', () => {
118+
it('types with array style emits definition', () => {
119+
const Emits = emits(['change'])
120+
class App extends Emits {
121+
mounted() {
122+
const vm = this
123+
equals<typeof vm.$emit, (event: 'change', ...args: any[]) => void>(true)
124+
}
125+
}
126+
})
127+
128+
it('types with object style emits definition', () => {
129+
const Emits = emits({
130+
change: (value: number) => true,
131+
input: (value: number, additional: string) => true
132+
})
133+
134+
class App extends Emits {
135+
mounted() {
136+
type ExpectedEmit
137+
= ((event: 'change', value: number) => void)
138+
& ((event: 'input', value: number, additional: string) => void)
139+
140+
141+
const vm = this
142+
equals<typeof vm.$emit, ExpectedEmit>(true)
143+
}
144+
}
145+
})
146+
147+
it('does not lose $props type', () => {
148+
const Emits = emits(['change'])
149+
class App extends Emits {
150+
mounted() {
151+
const vm = this
152+
equals<typeof vm.$props, {}>(true)
153+
}
154+
}
155+
})
156+
})

‎test-dts/tsconfig.json

Copy file name to clipboard
+12Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"extends": "../tsconfig.json",
3+
"compilerOptions": {
4+
"noEmit": true,
5+
"noUnusedLocals": false,
6+
"noUnusedParameters": false,
7+
"types": []
8+
},
9+
"include": [
10+
"**/*.ts"
11+
]
12+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.