node.
-const renderWithVuetify = (component, options, callback) => {
-  const root = document.createElement('div')
-  root.setAttribute('data-app', 'true')
+// // Custom container to integrate Vuetify with Vue Testing Library.
+// // Vuetify requires you to wrap your app with a v-app component that provides
+// // a 
 node.
+// const renderWithVuetify = (component, options, callback) => {
+//   const root = document.createElement('div')
+//   root.setAttribute('data-app', 'true')
 
-  return render(
-    component,
-    {
-      container: document.body.appendChild(root),
-      // for Vuetify components that use the $vuetify instance property
-      vuetify: new Vuetify(),
-      ...options,
-    },
-    callback,
-  )
-}
+//   return render(
+//     component,
+//     {
+//       container: document.body.appendChild(root),
+//       // for Vuetify components that use the $vuetify instance property
+//       vuetify: new Vuetify(),
+//       ...options,
+//     },
+//     callback,
+//   )
+// }
 
-test('should set [data-app] attribute on outer most div', () => {
-  const {container} = renderWithVuetify(VuetifyDemoComponent)
+// test('should set [data-app] attribute on outer most div', () => {
+//   const {container} = renderWithVuetify(VuetifyDemoComponent)
 
-  expect(container).toHaveAttribute('data-app', 'true')
-})
+//   expect(container).toHaveAttribute('data-app', 'true')
+// })
 
-test('renders a Vuetify-powered component', async () => {
-  const {getByText} = renderWithVuetify(VuetifyDemoComponent)
+// test('renders a Vuetify-powered component', async () => {
+//   const {getByText} = renderWithVuetify(VuetifyDemoComponent)
 
-  await fireEvent.click(getByText('open'))
+//   await fireEvent.click(getByText('open'))
 
-  expect(getByText('Lorem ipsum dolor sit amet.')).toMatchInlineSnapshot(`
-    
-      Lorem ipsum dolor sit amet.
-    
-  `)
-})
+//   expect(getByText('Lorem ipsum dolor sit amet.')).toMatchInlineSnapshot(`
+//     
+//       Lorem ipsum dolor sit amet.
+//     
+//   `)
+// })
 
-test('opens a menu', async () => {
-  const {getByRole, getByText, queryByText} = renderWithVuetify(
-    VuetifyDemoComponent,
-  )
+// test('opens a menu', async () => {
+//   const {getByRole, getByText, queryByText} = renderWithVuetify(
+//     VuetifyDemoComponent,
+//   )
 
-  const openMenuButton = getByRole('button', {name: 'open menu'})
+//   const openMenuButton = getByRole('button', {name: 'open menu'})
 
-  // Menu item is not rendered initially
-  expect(queryByText('menu item')).not.toBeInTheDocument()
+//   // Menu item is not rendered initially
+//   expect(queryByText('menu item')).not.toBeInTheDocument()
 
-  await fireEvent.click(openMenuButton)
+//   await fireEvent.click(openMenuButton)
 
-  const menuItem = getByText('menu item')
-  expect(menuItem).toBeInTheDocument()
+//   const menuItem = getByText('menu item')
+//   expect(menuItem).toBeInTheDocument()
 
-  await fireEvent.click(openMenuButton)
+//   await fireEvent.click(openMenuButton)
 
-  expect(menuItem).toBeInTheDocument()
-  expect(menuItem).not.toBeVisible()
-})
+//   expect(menuItem).toBeInTheDocument()
+//   expect(menuItem).not.toBeVisible()
+// })
diff --git a/src/__tests__/vuex.js b/src/__tests__/vuex.js
index f77206e6..7873f5bf 100644
--- a/src/__tests__/vuex.js
+++ b/src/__tests__/vuex.js
@@ -1,6 +1,5 @@
 import '@testing-library/jest-dom'
-import {render, fireEvent} from '@testing-library/vue'
-
+import {render, fireEvent} from '..'
 import VuexTest from './components/Store/VuexTest'
 import {store} from './components/Store/store'
 
@@ -19,6 +18,7 @@ function renderVuexTestComponent(customStore) {
 
 test('can render with vuex with defaults', async () => {
   const {getByTestId, getByText} = renderVuexTestComponent()
+
   await fireEvent.click(getByText('+'))
 
   expect(getByTestId('count-value')).toHaveTextContent('1')
@@ -28,6 +28,7 @@ test('can render with vuex with custom initial state', async () => {
   const {getByTestId, getByText} = renderVuexTestComponent({
     state: {count: 3},
   })
+
   await fireEvent.click(getByText('-'))
 
   expect(getByTestId('count-value')).toHaveTextContent('2')
@@ -44,8 +45,8 @@ test('can render with vuex with custom store', async () => {
     },
   }
 
-  // Notice how here we are not using the helper method, because there's no
-  // need to do that.
+  // Notice how here we are not using the helper rendering method, because
+  // there's no need to do that here. We're passing a whole store.
   const {getByTestId, getByText} = render(VuexTest, {store})
 
   await fireEvent.click(getByText('+'))
diff --git a/src/__tests__/within.js b/src/__tests__/within.js
index 245e4ed7..3a85664f 100644
--- a/src/__tests__/within.js
+++ b/src/__tests__/within.js
@@ -1,4 +1,4 @@
-import {render, within} from '@testing-library/vue'
+import {render, within} from '..'
 
 test('within() returns an object with all queries bound to the DOM node', () => {
   const {getByTestId, getByText} = render({
diff --git a/src/vue-testing-library.js b/src/index.js
similarity index 63%
rename from src/vue-testing-library.js
rename to src/index.js
index a7cd4472..e9b0ff9c 100644
--- a/src/vue-testing-library.js
+++ b/src/index.js
@@ -1,9 +1,10 @@
 /* eslint-disable testing-library/no-wait-for-empty-callback */
-import {createLocalVue, mount} from '@vue/test-utils'
+import {mount} from '@vue/test-utils'
+import merge from 'lodash.merge'
 
 import {
   getQueriesForElement,
-  logDOM,
+  prettyDOM,
   waitFor,
   fireEvent as dtlFireEvent,
 } from '@testing-library/dom'
@@ -19,73 +20,72 @@ function render(
     baseElement: customBaseElement,
     ...mountOptions
   } = {},
-  configurationCb,
 ) {
   const div = document.createElement('div')
   const baseElement = customBaseElement || customContainer || document.body
   const container = customContainer || baseElement.appendChild(div)
 
-  const attachTo = document.createElement('div')
-  container.appendChild(attachTo)
-
-  const localVue = createLocalVue()
-  let vuexStore = null
-  let router = null
-  let additionalOptions = {}
+  const plugins = []
 
   if (store) {
-    const Vuex = require('vuex')
-    localVue.use(Vuex)
-    vuexStore = new Vuex.Store(store)
+    const {createStore} = require('vuex')
+    plugins.push(createStore(store))
   }
 
   if (routes) {
     const requiredRouter = require('vue-router')
-    const VueRouter = requiredRouter.default || requiredRouter
-    localVue.use(VueRouter)
-    router = new VueRouter({
-      routes,
-    })
-  }
+    const {createRouter, createWebHistory} =
+      requiredRouter.default || requiredRouter
 
-  if (configurationCb && typeof configurationCb === 'function') {
-    additionalOptions = configurationCb(localVue, vuexStore, router)
+    const routerPlugin = createRouter({history: createWebHistory(), routes})
+    plugins.push(routerPlugin)
   }
 
-  if (!mountOptions.propsData && !!mountOptions.props) {
-    mountOptions.propsData = mountOptions.props
-    delete mountOptions.props
+  const mountComponent = (Component, newProps) => {
+    const wrapper = mount(
+      Component,
+      merge({
+        attachTo: container,
+        global: {plugins},
+        ...mountOptions,
+        props: newProps || mountOptions.props,
+      }),
+    )
+
+    // this removes the additional "data-v-app" div node from VTU:
+    // https://github.com/vuejs/vue-test-utils-next/blob/master/src/mount.ts#L196-L213
+    unwrapNode(wrapper.parentElement)
+
+    mountedWrappers.add(wrapper)
+    return wrapper
   }
 
-  const wrapper = mount(TestComponent, {
-    localVue,
-    router,
-    attachTo,
-    store: vuexStore,
-    ...mountOptions,
-    ...additionalOptions,
-  })
-
-  mountedWrappers.add(wrapper)
-  container.appendChild(wrapper.element)
+  let wrapper = mountComponent(TestComponent)
 
   return {
     container,
     baseElement,
-    debug: (el = baseElement) =>
-      Array.isArray(el) ? el.forEach(e => logDOM(e)) : logDOM(el),
-    unmount: () => wrapper.destroy(),
-    isUnmounted: () => wrapper.vm._isDestroyed,
+    debug: (el = baseElement, maxLength, options) =>
+      Array.isArray(el)
+        ? el.forEach(e => console.log(prettyDOM(e, maxLength, options)))
+        : console.log(prettyDOM(el, maxLength, options)),
+    unmount: () => wrapper.unmount(),
     html: () => wrapper.html(),
     emitted: () => wrapper.emitted(),
-    updateProps: _ => {
-      wrapper.setProps(_)
-      return waitFor(() => {})
+    rerender: ({props}) => {
+      wrapper.unmount()
+      mountedWrappers.delete(wrapper)
+
+      wrapper = mountComponent(TestComponent, props)
     },
     ...getQueriesForElement(baseElement),
   }
 }
 
+function unwrapNode(node) {
+  node.replaceWith(...node.childNodes)
+}
+
 function cleanup() {
   mountedWrappers.forEach(cleanupAtWrapper)
 }
@@ -98,11 +98,8 @@ function cleanupAtWrapper(wrapper) {
     document.body.removeChild(wrapper.element.parentNode)
   }
 
-  try {
-    wrapper.destroy()
-  } finally {
-    mountedWrappers.delete(wrapper)
-  }
+  wrapper.unmount()
+  mountedWrappers.delete(wrapper)
 }
 
 // Vue Testing Library's version of fireEvent will call DOM Testing Library's
@@ -114,8 +111,20 @@ async function fireEvent(...args) {
   await waitFor(() => {})
 }
 
+function suggestUpdateIfNecessary(eventValue, eventKey) {
+  const changeOrInputEventCalledDirectly =
+    eventValue && (eventKey === 'change' || eventKey === 'input')
+
+  if (changeOrInputEventCalledDirectly) {
+    console.warn(
+      `Using fireEvent.${eventKey}() may lead to unexpected results. Please use fireEvent.update() instead.`,
+    )
+  }
+}
+
 Object.keys(dtlFireEvent).forEach(key => {
   fireEvent[key] = async (...args) => {
+    suggestUpdateIfNecessary(args[1], key)
     dtlFireEvent[key](...args)
     await waitFor(() => {})
   }
@@ -149,6 +158,8 @@ fireEvent.update = (elem, value) => {
       if (['checkbox', 'radio'].includes(type)) {
         elem.checked = true
         return fireEvent.change(elem)
+      } else if (type === 'file') {
+        return fireEvent.change(elem)
       } else {
         elem.value = value
         return fireEvent.input(elem)
diff --git a/types/index.d.ts b/types/index.d.ts
new file mode 100644
index 00000000..c886447a
--- /dev/null
+++ b/types/index.d.ts
@@ -0,0 +1,66 @@
+// Minimum TypeScript Version: 4.0
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import {EmitsOptions} from 'vue'
+import {MountingOptions} from '@vue/test-utils'
+import {StoreOptions} from 'vuex'
+import {RouteRecordRaw} from 'vue-router'
+import {queries, EventType, BoundFunctions} from '@testing-library/dom'
+// eslint-disable-next-line import/no-extraneous-dependencies
+import {OptionsReceived as PrettyFormatOptions} from 'pretty-format'
+
+// NOTE: fireEvent is overridden below
+export * from '@testing-library/dom'
+
+type Debug = (
+  baseElement?: Element | DocumentFragment | Array
,
+  maxLength?: number,
+  options?: PrettyFormatOptions,
+) => void
+
+export interface RenderResult extends BoundFunctions {
+  container: Element
+  baseElement: Element
+  debug: Debug
+  unmount(): void
+  html(): string
+  emitted(): EmitsOptions
+  rerender(props: object): Promise
+}
+
+type VueTestUtilsRenderOptions = Omit<
+  MountingOptions>,
+  'attachTo' | 'shallow' | 'propsData'
+>
+type VueTestingLibraryRenderOptions = {
+  store?: StoreOptions<{}>
+  routes?: RouteRecordRaw[]
+  container?: Element
+  baseElement?: Element
+}
+type RenderOptions = VueTestUtilsRenderOptions & VueTestingLibraryRenderOptions
+
+export function render(
+  TestComponent: any, // this makes me sad :sob:
+  options?: RenderOptions,
+): RenderResult
+
+export type AsyncFireObject = {
+  [K in EventType]: (
+    element: Document | Element | Window,
+    options?: {},
+  ) => Promise
+}
+
+export interface VueFireEventObject extends AsyncFireObject {
+  (element: Document | Element | Window, event: Event): Promise
+  touch(element: Document | Element | Window): Promise
+  update(element: HTMLOptionElement): Promise
+  update(
+    element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement,
+    value: string,
+  ): Promise
+  update(element: Element, value?: string): Promise
+}
+
+export const fireEvent: VueFireEventObject
diff --git a/types/test.ts b/types/test.ts
new file mode 100644
index 00000000..aeb5df89
--- /dev/null
+++ b/types/test.ts
@@ -0,0 +1,98 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions */
+import {defineComponent} from 'vue'
+import {render, fireEvent, screen, waitFor} from '@testing-library/vue'
+
+declare const elem: Element
+
+const SomeComponent = defineComponent({
+  name: 'SomeComponent',
+  props: {
+    foo: {type: Number, default: 0},
+    bar: {type: String, default: '0'},
+  },
+})
+
+export async function testRender() {
+  const page = render({template: ''})
+
+  // single queries
+  page.getByText('foo')
+  page.queryByText('foo')
+  await page.findByText('foo')
+
+  // multiple queries
+  page.getAllByText('bar')
+  page.queryAllByText('bar')
+  await page.findAllByText('bar')
+
+  // helpers
+  const {container, baseElement, unmount, debug, rerender} = page
+
+  // eslint-disable-next-line @typescript-eslint/no-floating-promises
+  rerender({a: 1}) // $ExpectType Promise
+
+  debug() // $ExpectType void
+  debug(container) // $ExpectType void
+  debug([elem, elem], 100, {highlight: false}) // $ExpectType void
+
+  unmount() // $ExpectType void
+
+  container // $ExpectType Element
+  baseElement // $ExpectType Element
+}
+
+export function testRenderOptions() {
+  const container = document.createElement('div')
+  const baseElement = document.createElement('div')
+  const options = {container, baseElement}
+  render({template: 'div'}, options)
+}
+
+export async function testFireEvent() {
+  const {container} = render({template: 'button'})
+  await fireEvent.click(container) // $ExpectType Promise
+  await fireEvent.touch(elem) // $ExpectType Promise
+}
+
+export async function testScreen() {
+  render({template: 'button'})
+
+  await screen.findByRole('button') // $ExpectType Promise
+}
+
+export async function testWaitFor() {
+  const {container} = render({template: 'button'})
+  await fireEvent.update(container) // $ExpectType Promise
+  await waitFor(() => {})
+}
+
+export function testOptions() {
+  render(SomeComponent, {
+    attrs: {a: 1},
+    props: {c: 1}, // ideally it would fail because `c` is not an existing propβ¦
+    data: () => ({b: 2}),
+    slots: {
+      default: '',
+      footer: '',
+    },
+    global: {
+      config: {isCustomElement: _ => true},
+    },
+    store: {
+      state: {count: 3},
+      strict: true,
+    },
+    routes: [{path: '/', component: () => SomeComponent, name: 'route name'}],
+    baseElement: document.createElement('div'),
+    container: document.createElement('div'),
+  })
+}
+
+/*
+eslint
+  testing-library/prefer-explicit-assert: "off",
+  testing-library/no-wait-for-empty-callback: "off",
+  testing-library/no-debug: "off",
+  testing-library/prefer-screen-queries: "off",
+  @typescript-eslint/unbound-method: "off",
+*/
diff --git a/types/tsconfig.json b/types/tsconfig.json
new file mode 100644
index 00000000..d822978b
--- /dev/null
+++ b/types/tsconfig.json
@@ -0,0 +1,17 @@
+// this additional tsconfig is required by dtslint
+// see: https://github.com/Microsoft/dtslint#typestsconfigjson
+{
+  "compilerOptions": {
+    "module": "commonjs",
+    "lib": ["ES2020", "DOM"],
+    "noImplicitAny": true,
+    "noImplicitThis": true,
+    "strictNullChecks": true,
+    "strictFunctionTypes": true,
+    "noEmit": true,
+    "baseUrl": ".",
+    "paths": {
+      "@testing-library/vue": ["."]
+    }
+  }
+}
diff --git a/types/tslint.json b/types/tslint.json
new file mode 100644
index 00000000..70c4494b
--- /dev/null
+++ b/types/tslint.json
@@ -0,0 +1,7 @@
+{
+  "extends": "dtslint/dtslint.json",
+  "rules": {
+    "semicolon": false,
+    "whitespace": false
+  }
+}