ReviewOS

also looking at this

stacks/dtsx

fix(inference): keep the type parameters of an async arrow function

#3129
Open mkantautas wants to merge fix/async-generic-arrow into main
2 files +69 -2
packages/dtsx/src/processor/type-inference.tsmodified+13-2
Changes to packages/dtsx/src/processor/type-inference.ts
@@ -2419,7 +2419,18 @@ export function inferFunctionType(value: string, inUnion: boolean = false, _dept
24192419
24202420 // Handle async arrow functions
24212421 if (trimmed.startsWith('async ') && trimmed.includes('=>')) {
2422 const asyncRemoved = trimmed.slice(5).trim() // Remove 'async '
2422 let asyncRemoved = trimmed.slice(5).trim() // Remove 'async '
2423
2424 // Generics at the start, as for a regular arrow: `async <T>(a: T) => a`
2425 let generics = ''
2426 if (asyncRemoved.startsWith('<')) {
2427 const genericEnd = findMatchingBracket(asyncRemoved, 0, '<', '>')
2428 if (genericEnd !== -1) {
2429 generics = asyncRemoved.substring(0, genericEnd + 1)
2430 asyncRemoved = asyncRemoved.substring(genericEnd + 1).trim()
2431 }
2432 }
2433
24232434 const arrowIndex = findMainArrowIndex(asyncRemoved)
24242435 if (arrowIndex === -1) return inUnion ? '(() => Promise<unknown>)' : '() => Promise<unknown>'
24252436 const rawParams = asyncRemoved.substring(0, arrowIndex).trim()
@@ -2453,7 +2464,7 @@ export function inferFunctionType(value: string, inUnion: boolean = false, _dept
24532464 returnType = inferNarrowType(body, isConst, false, _depth + 1)
24542465 }
24552466
2456 const funcType = `${params} => ${explicitReturnType || returnType.startsWith('Promise<') ? returnType : `Promise<${returnType}>`}`
2467 const funcType = `${generics}${params} => ${explicitReturnType || returnType.startsWith('Promise<') ? returnType : `Promise<${returnType}>`}`
24572468 return inUnion ? `(${funcType})` : funcType
24582469 }
24592470
packages/dtsx/test/async-generic-arrow.test.tsadded+56-0
Changes to packages/dtsx/test/async-generic-arrow.test.ts
@@ -0,0 +1,56 @@
1/**
2 * An async arrow function with type parameters got them wrapped into its
3 * parameter list:
4 *
5 * export const load = async <T>(key: string, fallback: T): Promise<T> => ...
6 *
7 * emitted `(<T>(key: string, fallback: T): Promise<T>) => Promise<unknown>`,
8 * which does not parse. The async branch of inferFunctionType did not take
9 * leading type parameters off as the branch for other arrows does.
10 */
11import { describe, expect, it } from 'bun:test'
12import { mkdtemp, rm, writeFile } from 'node:fs/promises'
13import { tmpdir } from 'node:os'
14import { join } from 'node:path'
15import { runTsc } from './helpers/tsc'
16import { processCode } from './test-utils'
17
18/** Type-check `consumer` against the generated declarations as `./index`. */
19async function checkConsumer(dts: string, consumer: string): Promise<string> {
20 const dir = await mkdtemp(join(tmpdir(), 'dtsx-async-generic-'))
21 try {
22 await writeFile(join(dir, 'index.d.ts'), dts)
23 await writeFile(join(dir, 'use.ts'), consumer)
24 return runTsc(dir, ['index.d.ts', 'use.ts']).output
25 }
26 finally {
27 await rm(dir, { recursive: true, force: true })
28 }
29}
30
31describe('async arrow functions with type parameters', () => {
32 it.each([
33 [
34 'annotated return type',
35 'export const load = async <T>(key: string, fallback: T): Promise<T> => fallback\n',
36 'load: <T>(key: string, fallback: T) => Promise<T>;',
37 'import { load } from \'./index\'\nconst p: Promise<number> = load(\'k\', 1)\nexport {}\n',
38 ],
39 [
40 'constraint holding an arrow type and a block body',
41 'export const run = async <T, U extends () => void>(a: T, done: U): Promise<T> => { done(); return a }\n',
42 'run: <T, U extends () => void>(a: T, done: U) => Promise<T>;',
43 'import { run } from \'./index\'\nconst p: Promise<string> = run(\'x\', () => {})\nexport {}\n',
44 ],
45 [
46 'type parameter default',
47 'export const wrap = async <T = string>(a: T) => [a]\n',
48 'wrap: <T = string>(a: T) =>',
49 'import { wrap } from \'./index\'\nwrap(1)\nexport {}\n',
50 ],
51 ])('keeps them in front: %s', async (_label, code, signature, consumer) => {
52 const output = processCode(code)
53 expect(output).toContain(signature)
54 expect(await checkConsumer(output, consumer)).toBe('')
55 })
56})