also looking at this
fix(inference): keep the type parameters of a named function expression
#3130
2 files
+96
-1
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -2577,8 +2577,17 @@ export function inferFunctionType(value: string, inUnion: boolean = false, _dept | ||
| 2577 | 2577 | while (pos < len && trimmed.charCodeAt(pos) <= 32) pos++ |
| 2578 | 2578 | } |
| 2579 | 2579 | |
| 2580 | // Check for generics <...> | |
| 2580 | // Check for generics <...>, which follow the name if there is one: | |
| 2581 | // `function <T>(a: T)`, `function id<T>(a: T)`. `functionFactory<T>()` | |
| 2582 | // is a call, not a function expression with a name. | |
| 2581 | 2583 | let generics = '' |
| 2584 | let nameStart = -1 | |
| 2585 | if (!isWordChar(trimmed.charCodeAt(8))) { | |
| 2586 | const start = pos | |
| 2587 | while (pos < len && isWordChar(trimmed.charCodeAt(pos))) pos++ | |
| 2588 | if (pos > start) nameStart = start | |
| 2589 | while (pos < len && trimmed.charCodeAt(pos) <= 32) pos++ | |
| 2590 | } | |
| 2582 | 2591 | if (pos < len && trimmed.charCodeAt(pos) === 60) { // < |
| 2583 | 2592 | const genStart = pos |
| 2584 | 2593 | let depth = 1 |
| @@ -2593,6 +2602,14 @@ export function inferFunctionType(value: string, inUnion: boolean = false, _dept | ||
| 2593 | 2602 | generics = trimmed.substring(genStart, pos) |
| 2594 | 2603 | while (pos < len && trimmed.charCodeAt(pos) <= 32) pos++ |
| 2595 | 2604 | } |
| 2605 | // A named function's type parameters are kept only when the loop above | |
| 2606 | // closed them and they hold no quote or comment, which it does not skip | |
| 2607 | // (`<T extends '>'>`), and no `typeof`, which may name the function or | |
| 2608 | // its locals (`<T = typeof id>`); otherwise they are left out as before. | |
| 2609 | if (nameStart !== -1 && generics && (!generics.endsWith('>') || /['"`/]|\btypeof\b/.test(generics))) { | |
| 2610 | generics = '' | |
| 2611 | pos = nameStart | |
| 2612 | } | |
| 2596 | 2613 | |
| 2597 | 2614 | // Skip optional function name until ( |
| 2598 | 2615 | const parenIdx = trimmed.indexOf('(', pos) |
| @@ -0,0 +1,78 @@ | ||
| 1 | /** | |
| 2 | * A named function expression lost its type parameters: | |
| 3 | * | |
| 4 | * export const identity = function id<T>(value: T): T { return value } | |
| 5 | * | |
| 6 | * emitted `(value: T) => T`, where `T` is not declared (TS2304). The parser | |
| 7 | * looked for type parameters right after `function`, where an anonymous | |
| 8 | * function expression has them, not after the name. | |
| 9 | */ | |
| 10 | import { describe, expect, it } from 'bun:test' | |
| 11 | import { mkdtemp, rm, writeFile } from 'node:fs/promises' | |
| 12 | import { tmpdir } from 'node:os' | |
| 13 | import { join } from 'node:path' | |
| 14 | import { runTsc } from './helpers/tsc' | |
| 15 | import { processCode } from './test-utils' | |
| 16 | ||
| 17 | /** Type-check `consumer` against the generated declarations as `./index`. */ | |
| 18 | async function checkConsumer(dts: string, consumer: string): Promise<string> { | |
| 19 | const dir = await mkdtemp(join(tmpdir(), 'dtsx-generic-function-expression-')) | |
| 20 | try { | |
| 21 | await writeFile(join(dir, 'index.d.ts'), dts) | |
| 22 | await writeFile(join(dir, 'use.ts'), consumer) | |
| 23 | return runTsc(dir, ['index.d.ts', 'use.ts']).output | |
| 24 | } | |
| 25 | finally { | |
| 26 | await rm(dir, { recursive: true, force: true }) | |
| 27 | } | |
| 28 | } | |
| 29 | ||
| 30 | describe('function expressions with type parameters', () => { | |
| 31 | it.each([ | |
| 32 | [ | |
| 33 | 'named', | |
| 34 | 'export const identity = function id<T>(value: T): T { return value }\n', | |
| 35 | 'identity: <T>(value: T) => T;', | |
| 36 | 'import { identity } from \'./index\'\nconst n: number = identity(1)\nexport {}\n', | |
| 37 | ], | |
| 38 | [ | |
| 39 | 'named, with a constraint and a default', | |
| 40 | 'export const pick = function pick<K extends string, V = number>(key: K, value: V): V { return value }\n', | |
| 41 | 'pick: <K extends string, V = number>(key: K, value: V) => V;', | |
| 42 | 'import { pick } from \'./index\'\nconst s: string = pick(\'a\', \'b\')\nexport {}\n', | |
| 43 | ], | |
| 44 | [ | |
| 45 | 'named generator', | |
| 46 | 'export const repeat = function* repeat<T>(value: T): Generator<T> { yield value }\n', | |
| 47 | 'repeat: <T>(value: T) => Generator<T>;', | |
| 48 | 'import { repeat } from \'./index\'\nconst g: Generator<number> = repeat(1)\nexport {}\n', | |
| 49 | ], | |
| 50 | [ | |
| 51 | 'anonymous', | |
| 52 | 'export const anon = function <T>(value: T): T { return value }\n', | |
| 53 | 'anon: <T>(value: T) => T;', | |
| 54 | 'import { anon } from \'./index\'\nconst n: number = anon(1)\nexport {}\n', | |
| 55 | ], | |
| 56 | [ | |
| 57 | 'named, with a space before them', | |
| 58 | 'export const spaced = function id <T>(value: T): T { return value }\n', | |
| 59 | 'spaced: <T>(value: T) => T;', | |
| 60 | 'import { spaced } from \'./index\'\nconst n: number = spaced(1)\nexport {}\n', | |
| 61 | ], | |
| 62 | ])('keeps them: %s', async (_label, code, signature, consumer) => { | |
| 63 | const output = processCode(code) | |
| 64 | expect(output).toContain(signature) | |
| 65 | expect(await checkConsumer(output, consumer)).toBe('') | |
| 66 | }) | |
| 67 | ||
| 68 | it.each([ | |
| 69 | ['a quoted `>` in a constraint', 'export const f = function id<T extends \">\">(value: number): number { return value }\n', 'f: (value: number) => number;'], | |
| 70 | ['a quoted `<` in a constraint', 'export const f = function id<T extends \"<\">(value: number): number { return value }\n', 'f: (value: number) => number;'], | |
| 71 | ['a quoted `>` after a space', 'export const f = function id <T extends \'>\'>(value: number): number { return value }\n', 'f: (value: number) => number;'], | |
| 72 | ['a comment holding `>`', 'export const f = function id<T /* values > 0 */>(value: number): number { return value }\n', 'f: (value: number) => number;'], | |
| 73 | ['a type parameter naming the function itself', 'export const f = function id<T = typeof id>(value: number): number { return value }\n', 'f: (value: number) => number;'], | |
| 74 | ['a call to a function whose name starts with `function`', 'declare function functionFactory<T>(): () => unknown\nexport const f = functionFactory<string>()\n', 'f: () => unknown;'], | |
| 75 | ])('reads as before: %s', (_label, code, signature) => { | |
| 76 | expect(processCode(code)).toContain(signature) | |
| 77 | }) | |
| 78 | }) | |