Skip to content

Commit df13190

Browse files
committed
Fix(inquirer): Improve ReturnType of inquirer.prompt (not perfect, but better)
1 parent a9ee70f commit df13190

File tree

4 files changed

+65
-60
lines changed

4 files changed

+65
-60
lines changed

packages/inquirer/src/index.mts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
editor,
1616
Separator,
1717
} from '@inquirer/prompts';
18+
import type { Prettify } from '@inquirer/type';
1819
import { default as PromptsRunner } from './ui/prompt.mjs';
1920
import type {
2021
PromptCollection,
@@ -48,23 +49,21 @@ const defaultPrompts: PromptCollection = {
4849
* Create a new self-contained prompt module.
4950
*/
5051
export function createPromptModule(opt?: StreamOptions) {
51-
function promptModule<T extends Answers = Answers>(
52+
function promptModule<T extends Answers>(
5253
questions:
53-
| Question<T>
54+
| QuestionArray<T>
5455
| QuestionAnswerMap<T>
5556
| QuestionObservable<T>
56-
| QuestionArray<T>,
57+
| Question<T>,
5758
answers?: Partial<T>,
58-
): Promise<T> & { ui: PromptsRunner } {
59+
): Promise<Prettify<T>> & { ui: PromptsRunner<T> } {
5960
const runner = new PromptsRunner<T>(promptModule.prompts, opt);
6061

6162
try {
6263
return runner.run(questions, answers);
6364
} catch (error) {
64-
const promise = Promise.reject(error);
65-
// @ts-expect-error Monkey patch the UI on the promise object so
66-
promise.ui = runner;
67-
return promise as Promise<never> & { ui: PromptsRunner };
65+
const promise = Promise.reject<T>(error);
66+
return Object.assign(promise, { ui: runner });
6867
}
6968
}
7069

@@ -85,7 +84,7 @@ export function createPromptModule(opt?: StreamOptions) {
8584
* Register the defaults provider prompts
8685
*/
8786
promptModule.restoreDefaultPrompts = function () {
88-
this.prompts = { ...defaultPrompts };
87+
promptModule.prompts = { ...defaultPrompts };
8988
};
9089

9190
promptModule.restoreDefaultPrompts();

packages/inquirer/src/types.mts

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,43 +13,55 @@ import {
1313
import type { Prettify } from '@inquirer/type';
1414
import { Observable } from 'rxjs';
1515

16-
export type Answers = { [key: string]: any };
17-
18-
interface QuestionMap {
19-
input: { type: 'input' } & Parameters<typeof input>[0];
20-
select: { type: 'select' } & Parameters<typeof select>[0];
21-
/** @deprecated Prompt type `list` is renamed to `select` */
22-
list: { type: 'list' } & Parameters<typeof select>[0];
23-
number: { type: 'number' } & Parameters<typeof number>[0];
24-
confirm: { type: 'confirm' } & Parameters<typeof confirm>[0];
25-
rawlist: { type: 'rawlist' } & Parameters<typeof rawlist>[0];
26-
expand: { type: 'expand' } & Parameters<typeof expand>[0];
27-
checkbox: { type: 'checkbox' } & Parameters<typeof checkbox>[0];
28-
password: { type: 'password' } & Parameters<typeof password>[0];
29-
editor: { type: 'editor' } & Parameters<typeof editor>[0];
30-
}
16+
// eslint-disable-next-line @typescript-eslint/ban-types
17+
type LiteralUnion<T extends F, F = string> = T | (F & {});
18+
type KeyUnion<T> = LiteralUnion<Extract<keyof T, string>>;
19+
20+
export type Answers = {
21+
[key: string]: any;
22+
};
3123

32-
type whenFunction<T extends Answers = Answers> =
24+
type whenFunction<T extends Answers> =
3325
| ((answers: Partial<T>) => boolean | Promise<boolean>)
3426
| ((this: { async: () => () => void }, answers: Partial<T>) => void);
3527

36-
type InquirerFields<T extends Answers = Answers> = {
37-
name: keyof T;
28+
type InquirerFields<T extends Answers> = {
29+
name: KeyUnion<T>;
3830
when?: boolean | whenFunction<T>;
3931
askAnswered?: boolean;
4032
};
4133

42-
export type Question<T extends Answers = Answers> = QuestionMap[keyof QuestionMap] &
43-
InquirerFields<T>;
34+
interface QuestionMap<T extends Answers> {
35+
input: Prettify<{ type: 'input' } & Parameters<typeof input>[0] & InquirerFields<T>>;
36+
select: Prettify<{ type: 'select' } & Parameters<typeof select>[0] & InquirerFields<T>>;
37+
list: Prettify<{ type: 'list' } & Parameters<typeof select>[0] & InquirerFields<T>>;
38+
number: Prettify<{ type: 'number' } & Parameters<typeof number>[0] & InquirerFields<T>>;
39+
confirm: Prettify<
40+
{ type: 'confirm' } & Parameters<typeof confirm>[0] & InquirerFields<T>
41+
>;
42+
rawlist: Prettify<
43+
{ type: 'rawlist' } & Parameters<typeof rawlist>[0] & InquirerFields<T>
44+
>;
45+
expand: Prettify<{ type: 'expand' } & Parameters<typeof expand>[0] & InquirerFields<T>>;
46+
checkbox: Prettify<
47+
{ type: 'checkbox' } & Parameters<typeof checkbox>[0] & InquirerFields<T>
48+
>;
49+
password: Prettify<
50+
{ type: 'password' } & Parameters<typeof password>[0] & InquirerFields<T>
51+
>;
52+
editor: Prettify<{ type: 'editor' } & Parameters<typeof editor>[0] & InquirerFields<T>>;
53+
}
54+
55+
export type Question<T extends Answers> = QuestionMap<T>[keyof QuestionMap<T>];
4456

45-
export type QuestionAnswerMap<T extends Answers = Answers> = Record<
46-
keyof T,
47-
Omit<Question<T>, 'name'>
57+
export type QuestionAnswerMap<T extends Answers> = Record<
58+
KeyUnion<T>,
59+
Prettify<Omit<Question<T>, 'name'>>
4860
>;
4961

50-
export type QuestionArray<T extends Answers = Answers> = Array<Question<T>>;
62+
export type QuestionArray<T extends Answers> = Question<T>[];
5163

52-
export type QuestionObservable<T extends Answers = Answers> = Observable<Question<T>>;
64+
export type QuestionObservable<T extends Answers> = Observable<Question<T>>;
5365

5466
export type StreamOptions = Prettify<
5567
Parameters<typeof input>[1] & { skipTTYChecks?: boolean }

packages/inquirer/src/ui/prompt.mts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,8 @@ function setupReadlineOptions(opt: StreamOptions = {}) {
161161
};
162162
}
163163

164-
function isQuestionMap<T extends Answers = Answers>(
165-
questions: Question<T> | QuestionAnswerMap<T> | QuestionArray<T>,
164+
function isQuestionMap<T extends Answers>(
165+
questions: QuestionArray<T> | QuestionAnswerMap<T> | Question<T>,
166166
): questions is QuestionAnswerMap<T> {
167167
return Object.values(questions).every(
168168
(maybeQuestion) =>
@@ -185,7 +185,7 @@ function isPromptConstructor(
185185
/**
186186
* Base interface class other can inherits from
187187
*/
188-
export default class PromptsRunner<T extends Answers = Answers> extends Base {
188+
export default class PromptsRunner<T extends Answers> extends Base {
189189
prompts: PromptCollection;
190190
answers: Partial<T> = {};
191191
process: Observable<any>;
@@ -202,12 +202,12 @@ export default class PromptsRunner<T extends Answers = Answers> extends Base {
202202

203203
run(
204204
questions:
205-
| Question<T>
205+
| QuestionArray<T>
206206
| QuestionAnswerMap<T>
207207
| QuestionObservable<T>
208-
| QuestionArray<T>,
208+
| Question<T>,
209209
answers?: Partial<T>,
210-
): Promise<T> & { ui: PromptsRunner } {
210+
): Promise<T> & { ui: PromptsRunner<T> } {
211211
// Keep global reference to the answers
212212
this.answers = typeof answers === 'object' ? { ...answers } : {};
213213

@@ -244,11 +244,9 @@ export default class PromptsRunner<T extends Answers = Answers> extends Base {
244244
).then(
245245
() => this.onCompletion(),
246246
(error) => this.onError(error),
247-
);
247+
) as Promise<T>;
248248

249-
// @ts-expect-error Monkey patch the UI on the promise object so
250-
promise.ui = this;
251-
return promise as Promise<T> & { ui: PromptsRunner };
249+
return Object.assign(promise, { ui: this });
252250
}
253251

254252
/**
@@ -386,7 +384,7 @@ export default class PromptsRunner<T extends Answers = Answers> extends Base {
386384
}
387385
return;
388386
}),
389-
).pipe(filter((val): val is Question => val != null)),
387+
).pipe(filter((val): val is Question<T> => val != null)),
390388
);
391389
}
392390
}

packages/inquirer/test/inquirer.test.mts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,10 @@ describe('inquirer.prompt', () => {
156156
});
157157

158158
it('should take a single prompt and return answer', async () => {
159-
const config = {
159+
const answers = await inquirer.prompt({
160160
type: 'stub',
161161
name: 'q1',
162-
};
163-
164-
const answers = await inquirer.prompt(config);
162+
});
165163
expect(answers).toEqual({ q1: 'bar' });
166164
});
167165

@@ -352,7 +350,7 @@ describe('inquirer.prompt', () => {
352350
}
353351
inquirer.registerPrompt('stubSelect', FakeSelect);
354352

355-
const prompts = [
353+
await inquirer.prompt([
356354
{
357355
type: 'stub',
358356
name: 'name1',
@@ -363,19 +361,18 @@ describe('inquirer.prompt', () => {
363361
type: 'stubSelect',
364362
name: 'name',
365363
message: 'message',
366-
choices(answers) {
364+
choices(answers: { name1: string }) {
367365
expect(answers.name1).toEqual('bar');
368366
return stubChoices;
369367
},
370368
},
371-
];
372-
373-
await inquirer.prompt(prompts);
369+
]);
374370
});
375371

376372
it('should expose the Reactive interface', async () => {
377373
const spy = vi.fn();
378-
const prompts = [
374+
375+
const promise = inquirer.prompt([
379376
{
380377
type: 'stub',
381378
name: 'name1',
@@ -388,9 +385,7 @@ describe('inquirer.prompt', () => {
388385
message: 'message',
389386
answer: 'doe',
390387
},
391-
];
392-
393-
const promise = inquirer.prompt(prompts);
388+
]);
394389
promise.ui.process.subscribe(spy);
395390

396391
await promise;
@@ -616,14 +611,15 @@ describe('inquirer.prompt', () => {
616611
});
617612

618613
it('should not run prompt if nested answer exists for question', async () => {
619-
const answers = await inquirer.prompt(
614+
const answers = await inquirer.prompt<{ prefilled: { nested: string } }>(
620615
[
621616
{
622617
type: 'input',
623618
name: 'prefilled.nested',
624619
when: throwFunc.bind(undefined, 'when'),
625620
validate: throwFunc.bind(undefined, 'validate'),
626621
transformer: throwFunc.bind(undefined, 'transformer'),
622+
// @ts-expect-error ignoring this unused field for test purpose
627623
filter: throwFunc.bind(undefined, 'filter'),
628624
message: 'message',
629625
default: 'newValue',
@@ -633,7 +629,7 @@ describe('inquirer.prompt', () => {
633629
prefilled: { nested: 'prefilled' },
634630
},
635631
);
636-
expect(answers['prefilled'].nested).toEqual('prefilled');
632+
expect(answers.prefilled.nested).toEqual('prefilled');
637633
});
638634

639635
it('should run prompt if answer exists for question and askAnswered is set', async () => {

0 commit comments

Comments
 (0)