forked from nuxt/nuxt
-
Notifications
You must be signed in to change notification settings - Fork 0
/
types.ts
383 lines (339 loc) · 19.2 KB
/
types.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
import { describe, expectTypeOf, it } from 'vitest'
import type { Ref } from 'vue'
import type { FetchError } from 'ofetch'
import type { NavigationFailure, RouteLocationNormalizedLoaded, RouteLocationRaw, Router, useRouter as vueUseRouter } from '#vue-router'
import type { AppConfig, RuntimeValue } from 'nuxt/schema'
import { defineNuxtConfig } from 'nuxt/config'
import { callWithNuxt, isVue3 } from '#app'
import type { NavigateToOptions } from '#app/composables/router'
import { NuxtLink, NuxtPage } from '#components'
import { useRouter } from '#imports'
interface TestResponse { message: string }
describe('API routes', () => {
it('generates types for routes', () => {
expectTypeOf($fetch('/api/hello')).toEqualTypeOf<Promise<string>>()
expectTypeOf($fetch('/api/hey')).toEqualTypeOf<Promise<{ foo: string, baz: string }>>()
expectTypeOf($fetch('/api/hey', { method: 'get' })).toEqualTypeOf<Promise<{ foo: string, baz: string }>>()
expectTypeOf($fetch('/api/hey', { method: 'post' })).toEqualTypeOf<Promise<{ method: 'post' }>>()
// @ts-expect-error not a valid method
expectTypeOf($fetch('/api/hey', { method: 'patch ' })).toEqualTypeOf<Promise<{ foo: string, baz: string }>>()
expectTypeOf($fetch('/api/union')).toEqualTypeOf<Promise<{ type: 'a', foo: string } | { type: 'b', baz: string }>>()
expectTypeOf($fetch('/api/other')).toEqualTypeOf<Promise<unknown>>()
expectTypeOf($fetch<TestResponse>('/test')).toEqualTypeOf<Promise<TestResponse>>()
})
it('works with useAsyncData', () => {
expectTypeOf(useAsyncData('api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useAsyncData('api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useAsyncData('api-hey-with-pick', () => $fetch('/api/hey'), { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | null>>()
expectTypeOf(useAsyncData('api-union', () => $fetch('/api/union')).data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | null>>()
expectTypeOf(useAsyncData('api-union-with-pick', () => $fetch('/api/union'), { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | null>>()
expectTypeOf(useAsyncData('api-other', () => $fetch('/api/other')).data).toEqualTypeOf<Ref<unknown>>()
expectTypeOf(useAsyncData<TestResponse>('api-generics', () => $fetch('/test')).data).toEqualTypeOf<Ref<TestResponse | null>>()
expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | null>>()
expectTypeOf(useAsyncData<any, string>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyAsyncData('lazy-api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyAsyncData('lazy-api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useLazyAsyncData('lazy-api-hey-with-pick', () => $fetch('/api/hey'), { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | null>>()
expectTypeOf(useLazyAsyncData('lazy-api-union', () => $fetch('/api/union')).data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | null>>()
expectTypeOf(useLazyAsyncData('lazy-api-union-with-pick', () => $fetch('/api/union'), { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | null>>()
expectTypeOf(useLazyAsyncData('lazy-api-other', () => $fetch('/api/other')).data).toEqualTypeOf<Ref<unknown>>()
expectTypeOf(useLazyAsyncData<TestResponse>('lazy-api-generics', () => $fetch('/test')).data).toEqualTypeOf<Ref<TestResponse | null>>()
expectTypeOf(useLazyAsyncData('lazy-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | null>>()
expectTypeOf(useLazyAsyncData<any, string>('lazy-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<string | null>>()
})
it('works with useFetch', () => {
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
// @ts-expect-error TODO: remove when fixed upstream: https://github.com/unjs/nitro/pull/1247
expectTypeOf(useFetch('/api/hey', { method: 'GET' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useFetch('/api/hey', { method: 'get' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useFetch('/api/hey', { method: 'post' }).data).toEqualTypeOf<Ref<{ method: 'post' } | null>>()
// @ts-expect-error not a valid method
useFetch('/api/hey', { method: 'PATCH' })
expectTypeOf(useFetch('/api/hey', { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | null>>()
expectTypeOf(useFetch('/api/union').data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | null>>()
expectTypeOf(useFetch('/api/union', { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | null>>()
expectTypeOf(useFetch('/api/other').data).toEqualTypeOf<Ref<unknown>>()
expectTypeOf(useFetch<TestResponse>('/test').data).toEqualTypeOf<Ref<TestResponse | null>>()
expectTypeOf(useFetch<TestResponse>('/test', { method: 'POST' }).data).toEqualTypeOf<Ref<TestResponse | null>>()
expectTypeOf(useFetch('/error').error).toEqualTypeOf<Ref<FetchError | null>>()
expectTypeOf(useFetch<any, string>('/error').error).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useLazyFetch('/api/hey', { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | null>>()
expectTypeOf(useLazyFetch('/api/union').data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | null>>()
expectTypeOf(useLazyFetch('/api/union', { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | null>>()
expectTypeOf(useLazyFetch('/api/other').data).toEqualTypeOf<Ref<unknown>>()
expectTypeOf(useLazyFetch<TestResponse>('/test').data).toEqualTypeOf<Ref<TestResponse | null>>()
expectTypeOf(useLazyFetch('/error').error).toEqualTypeOf<Ref<FetchError | null>>()
expectTypeOf(useLazyFetch<any, string>('/error').error).toEqualTypeOf<Ref<string | null>>()
})
})
describe('aliases', () => {
it('allows importing from path aliases', () => {
expectTypeOf(useRouter).toEqualTypeOf<typeof vueUseRouter>()
expectTypeOf(isVue3).toEqualTypeOf<boolean>()
})
})
describe('middleware', () => {
it('recognizes named middleware', () => {
definePageMeta({ middleware: 'inject-auth' })
// @ts-expect-error ignore global middleware
definePageMeta({ middleware: 'redirect' })
// @ts-expect-error Invalid middleware
definePageMeta({ middleware: 'invalid-middleware' })
})
it('handles adding middleware', () => {
addRouteMiddleware('example', (to, from) => {
expectTypeOf(to).toEqualTypeOf<RouteLocationNormalizedLoaded>()
expectTypeOf(from).toEqualTypeOf<RouteLocationNormalizedLoaded>()
expectTypeOf(navigateTo).toEqualTypeOf<(to: RouteLocationRaw | null | undefined, options?: NavigateToOptions) => RouteLocationRaw | void | false | Promise<void | NavigationFailure | false>>()
navigateTo('/')
abortNavigation()
abortNavigation('error string')
abortNavigation(new Error('my error'))
// @ts-expect-error Must return error or string
abortNavigation(true)
}, { global: true })
})
})
describe('typed router integration', () => {
it('allows typing useRouter', () => {
const router = useRouter()
// @ts-expect-error this named route does not exist
router.push({ name: 'some-thing' })
// this one does
router.push({ name: 'fixed-keyed-child-parent' })
// @ts-expect-error this is an invalid param
router.push({ name: 'random-id', params: { bob: 23 } })
router.push({ name: 'random-id', params: { id: 4 } })
})
it('allows typing useRoute', () => {
const route = useRoute('random-id')
// @ts-expect-error this param does not exist
const _invalid = route.params.something
// this param does
const _valid = route.params.id
})
it('allows typing navigateTo', () => {
// @ts-expect-error this named route does not exist
navigateTo({ name: 'some-thing' })
// this one does
navigateTo({ name: 'fixed-keyed-child-parent' })
// @ts-expect-error this is an invalid param
navigateTo({ name: 'random-id', params: { bob: 23 } })
navigateTo({ name: 'random-id', params: { id: 4 } })
})
it('allows typing middleware', () => {
defineNuxtRouteMiddleware((to) => {
expectTypeOf(to.name).not.toBeAny()
// @ts-expect-error this route does not exist
expectTypeOf(to.name === 'bob').toMatchTypeOf<boolean>()
expectTypeOf(to.name === 'assets').toMatchTypeOf<boolean>()
})
})
it('respects pages:extend augmentation', () => {
// added via pages:extend
expectTypeOf(useRoute().name === 'internal-async-parent').toMatchTypeOf<boolean>()
// @ts-expect-error this route does not exist
expectTypeOf(useRoute().name === 'invalid').toMatchTypeOf<boolean>()
})
it('allows typing NuxtLink', () => {
// @ts-expect-error this named route does not exist
h(NuxtLink, { to: { name: 'some-thing' } })
// this one does
h(NuxtLink, { to: { name: 'fixed-keyed-child-parent' } })
// @ts-expect-error this is an invalid param
h(NuxtLink, { to: { name: 'random-id', params: { bob: 23 } } })
h(NuxtLink, { to: { name: 'random-id', params: { id: 4 } } })
})
})
describe('layouts', () => {
it('recognizes named layouts', () => {
definePageMeta({ layout: 'custom' })
definePageMeta({ layout: 'pascal-case' })
// @ts-expect-error Invalid layout
definePageMeta({ layout: 'invalid-layout' })
})
})
describe('modules', () => {
it('augments schema automatically', () => {
defineNuxtConfig({ sampleModule: { enabled: false } })
// @ts-expect-error we want to ensure we throw type error on invalid option
defineNuxtConfig({ sampleModule: { other: false } })
// @ts-expect-error we want to ensure we throw type error on invalid key
defineNuxtConfig({ undeclaredKey: { other: false } })
})
})
describe('nuxtApp', () => {
it('types injections provided by plugins', () => {
expectTypeOf(useNuxtApp().$asyncPlugin).toEqualTypeOf<() => string>()
expectTypeOf(useNuxtApp().$router).toEqualTypeOf<Router>()
})
it('marks unknown injections as unknown', () => {
expectTypeOf(useNuxtApp().doesNotExist).toEqualTypeOf<unknown>()
expectTypeOf(useNuxtApp().$random).toEqualTypeOf<unknown>()
})
})
describe('runtimeConfig', () => {
it('generated runtimeConfig types', () => {
const runtimeConfig = useRuntimeConfig()
expectTypeOf(runtimeConfig.public.testConfig).toEqualTypeOf<number>()
expectTypeOf(runtimeConfig.public.needsFallback).toEqualTypeOf<string>()
expectTypeOf(runtimeConfig.privateConfig).toEqualTypeOf<string>()
expectTypeOf(runtimeConfig.public.ids).toEqualTypeOf<number[]>()
expectTypeOf(runtimeConfig.unknown).toEqualTypeOf<any>()
const injectedConfig = useNuxtApp().$config
expectTypeOf(injectedConfig.public.testConfig).toEqualTypeOf<number>()
expectTypeOf(injectedConfig.public.needsFallback).toEqualTypeOf<string>()
expectTypeOf(injectedConfig.privateConfig).toEqualTypeOf<string>()
expectTypeOf(injectedConfig.public.ids).toEqualTypeOf<number[]>()
expectTypeOf(injectedConfig.unknown).toEqualTypeOf<any>()
})
it('provides hints on overriding these values', () => {
const val = defineNuxtConfig({
runtimeConfig: {
public: {
// @ts-expect-error this should be a number
testConfig: 'test',
ids: [1, 2]
}
}
})
expectTypeOf(val.runtimeConfig!.public!.testConfig).toEqualTypeOf<undefined | RuntimeValue<number, 'You can override this value at runtime with NUXT_PUBLIC_TEST_CONFIG'>>()
expectTypeOf(val.runtimeConfig!.privateConfig).toEqualTypeOf<undefined | RuntimeValue<string, 'You can override this value at runtime with NUXT_PRIVATE_CONFIG'>>()
expectTypeOf(val.runtimeConfig!.baseURL).toEqualTypeOf<undefined | RuntimeValue<string, 'You can override this value at runtime with NUXT_BASE_URL'>>()
expectTypeOf(val.runtimeConfig!.baseAPIToken).toEqualTypeOf<undefined | RuntimeValue<string, 'You can override this value at runtime with NUXT_BASE_API_TOKEN'>>()
expectTypeOf(val.runtimeConfig!.public!.ids).toEqualTypeOf<undefined | RuntimeValue<Array<number | undefined>, 'You can override this value at runtime with NUXT_PUBLIC_IDS'>>()
expectTypeOf(val.runtimeConfig!.unknown).toEqualTypeOf<any>()
})
})
describe('head', () => {
it('correctly types nuxt.config options', () => {
defineNuxtConfig({ app: { head: { titleTemplate: () => 'test' } } })
defineNuxtConfig({
app: {
head: {
meta: [{ key: 'key', name: 'description', content: 'some description ' }],
titleTemplate: 'test %s'
}
}
})
})
it('types useHead', () => {
useHead({
base: { href: '/base' },
link: computed(() => []),
meta: [
{ key: 'key', name: 'description', content: 'some description ' },
() => ({ key: 'key', name: 'description', content: 'some description ' })
],
titleTemplate: (titleChunk) => {
return titleChunk ? `${titleChunk} - Site Title` : 'Site Title'
}
})
})
})
describe('components', () => {
it('includes types for NuxtPage', () => {
expectTypeOf(NuxtPage).not.toBeAny()
})
})
describe('composables', () => {
it('allows providing default refs', () => {
expectTypeOf(useState('test', () => ref('hello'))).toEqualTypeOf<Ref<string>>()
expectTypeOf(useState('test', () => 'hello')).toEqualTypeOf<Ref<string>>()
expectTypeOf(useCookie('test', { default: () => ref(500) })).toEqualTypeOf<Ref<number>>()
expectTypeOf(useCookie('test', { default: () => 500 })).toEqualTypeOf<Ref<number>>()
useCookie<number | null>('test').value = null
expectTypeOf(useAsyncData('test', () => Promise.resolve(500), { default: () => ref(500) }).data).toEqualTypeOf<Ref<number | null>>()
expectTypeOf(useAsyncData('test', () => Promise.resolve(500), { default: () => 500 }).data).toEqualTypeOf<Ref<number | null>>()
expectTypeOf(useAsyncData('test', () => Promise.resolve('500'), { default: () => ref(500) }).data).toEqualTypeOf<Ref<number | null>>()
expectTypeOf(useAsyncData('test', () => Promise.resolve('500'), { default: () => 500 }).data).toEqualTypeOf<Ref<number | null>>()
expectTypeOf(useFetch('/test', { default: () => ref(500) }).data).toEqualTypeOf<Ref<number | null>>()
expectTypeOf(useFetch('/test', { default: () => 500 }).data).toEqualTypeOf<Ref<number | null>>()
})
it('infer request url string literal from server/api routes', () => {
// request can accept dynamic string type
const dynamicStringUrl = 'https://example.com/api'
expectTypeOf(useFetch(dynamicStringUrl).data).toEqualTypeOf<Ref<unknown>>()
// request param should infer string literal type / show auto-complete hint base on server routes, ex: '/api/hello'
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
// request can accept string literal and Request object type
expectTypeOf(useFetch('https://example.com/api').data).toEqualTypeOf<Ref<unknown>>()
expectTypeOf(useFetch(new Request('test')).data).toEqualTypeOf<Ref<unknown>>()
})
it('provides proper type support when using overloads', () => {
expectTypeOf(useState('test')).toEqualTypeOf(useState())
expectTypeOf(useState('test', () => ({ foo: Math.random() }))).toEqualTypeOf(useState(() => ({ foo: Math.random() })))
expectTypeOf(useAsyncData('test', () => Promise.resolve({ foo: Math.random() })))
.toEqualTypeOf(useAsyncData(() => Promise.resolve({ foo: Math.random() })))
expectTypeOf(useAsyncData('test', () => Promise.resolve({ foo: Math.random() }), { transform: data => data.foo }))
.toEqualTypeOf(useAsyncData(() => Promise.resolve({ foo: Math.random() }), { transform: data => data.foo }))
expectTypeOf(useLazyAsyncData('test', () => Promise.resolve({ foo: Math.random() })))
.toEqualTypeOf(useLazyAsyncData(() => Promise.resolve({ foo: Math.random() })))
expectTypeOf(useLazyAsyncData('test', () => Promise.resolve({ foo: Math.random() }), { transform: data => data.foo }))
.toEqualTypeOf(useLazyAsyncData(() => Promise.resolve({ foo: Math.random() }), { transform: data => data.foo }))
// Default values: #14437
expectTypeOf(useAsyncData('test', () => Promise.resolve({ foo: { bar: 500 } }), { default: () => ({ bar: 500 }), transform: v => v.foo }).data).toEqualTypeOf<Ref<{ bar: number } | null>>()
expectTypeOf(useLazyAsyncData('test', () => Promise.resolve({ foo: { bar: 500 } }), { default: () => ({ bar: 500 }), transform: v => v.foo }))
.toEqualTypeOf(useLazyAsyncData(() => Promise.resolve({ foo: { bar: 500 } }), { default: () => ({ bar: 500 }), transform: v => v.foo }))
expectTypeOf(useFetch('/api/hey', { default: () => 'bar', transform: v => v.foo }).data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyFetch('/api/hey', { default: () => 'bar', transform: v => v.foo }).data).toEqualTypeOf<Ref<string | null>>()
})
it('uses types compatible between useRequestHeaders and useFetch', () => {
useFetch('/api/hey', {
headers: useRequestHeaders()
})
useFetch('/api/hey', {
headers: useRequestHeaders(['test'])
})
const { test } = useRequestHeaders(['test'])
expectTypeOf(test).toEqualTypeOf<string | undefined>()
})
it('correctly types returns with key signatures', () => {
interface TestType {
id: string
content: string[]
[x: string]: any
}
const testFetch = () => Promise.resolve({}) as Promise<TestType>
const { data: notTypedData } = useAsyncData('test', testFetch)
expectTypeOf(notTypedData.value!.id).toEqualTypeOf<string>()
expectTypeOf(notTypedData.value!.content).toEqualTypeOf<string[]>()
expectTypeOf(notTypedData.value!.untypedKey).toEqualTypeOf<any>()
})
})
describe('app config', () => {
it('merges app config as expected', () => {
interface ExpectedMergedAppConfig {
fromLayer: boolean
fromNuxtConfig: boolean
nested: {
val: number
}
userConfig: 123 | 456
someThing?: {
value?: string | false,
}
[key: string]: unknown
}
expectTypeOf<AppConfig>().toEqualTypeOf<ExpectedMergedAppConfig>()
})
})
describe('extends type declarations', () => {
it('correctly adds references to tsconfig', () => {
expectTypeOf<import('bing').BingInterface>().toEqualTypeOf<{ foo: 'bar' }>()
})
})
describe('composables inference', () => {
it('callWithNuxt', () => {
const bob = callWithNuxt({} as any, () => true)
expectTypeOf<typeof bob>().toEqualTypeOf<boolean | Promise<boolean>>()
})
it('runWithContext', () => {
const bob = useNuxtApp().runWithContext(() => true)
expectTypeOf<typeof bob>().toEqualTypeOf<boolean | Promise<boolean>>()
})
})