also looking at this
fix(inference): keep the type parameters of an async arrow function
#3129
2 files
+69
-2
| @@ -2419,7 +2419,18 @@ export function inferFunctionType(value: string, inUnion: boolean = false, _dept | ||
| 2419 | 2419 | |
| 2420 | 2420 | // Handle async arrow functions |
| 2421 | 2421 | 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 | ||
| 2423 | 2434 | const arrowIndex = findMainArrowIndex(asyncRemoved) |
| 2424 | 2435 | if (arrowIndex === -1) return inUnion ? '(() => Promise<unknown>)' : '() => Promise<unknown>' |
| 2425 | 2436 | const rawParams = asyncRemoved.substring(0, arrowIndex).trim() |
| @@ -2453,7 +2464,7 @@ export function inferFunctionType(value: string, inUnion: boolean = false, _dept | ||
| 2453 | 2464 | returnType = inferNarrowType(body, isConst, false, _depth + 1) |
| 2454 | 2465 | } |
| 2455 | 2466 | |
| 2456 | const funcType = `${params} => ${explicitReturnType || returnType.startsWith('Promise<') ? returnType : `Promise<${returnType}>`}` | |
| 2467 | const funcType = `${generics}${params} => ${explicitReturnType || returnType.startsWith('Promise<') ? returnType : `Promise<${returnType}>`}` | |
| 2457 | 2468 | return inUnion ? `(${funcType})` : funcType |
| 2458 | 2469 | } |
| 2459 | 2470 | |
| @@ -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 | */ | |
| 11 | import { describe, expect, it } from 'bun:test' | |
| 12 | import { mkdtemp, rm, writeFile } from 'node:fs/promises' | |
| 13 | import { tmpdir } from 'node:os' | |
| 14 | import { join } from 'node:path' | |
| 15 | import { runTsc } from './helpers/tsc' | |
| 16 | import { processCode } from './test-utils' | |
| 17 | ||
| 18 | /** Type-check `consumer` against the generated declarations as `./index`. */ | |
| 19 | async 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 | ||
| 31 | describe('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 | }) | |