Skip to content

Commit d44121e

Browse files
authored
fix: generics & inferences (#153)
1 parent 064018f commit d44121e

File tree

9 files changed

+105
-35
lines changed

9 files changed

+105
-35
lines changed

README.md

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const name = await question("What's your name ?", {
9494
### `select()`
9595

9696
```ts
97-
select(message: string, options: SelectOptions): Promise<string>
97+
select<T extends string>(message: string, options: SelectOptions<T>): Promise<T>
9898
```
9999
100100
Scrollable select depending `maxVisible` (default `8`).
@@ -114,7 +114,7 @@ Use `options.skip` to skip prompt. It will return the first choice.
114114
### `multiselect()`
115115
116116
```ts
117-
multiselect(message: string, options: MultiselectOptions): Promise<[string]>
117+
multiselect<T extends string>(message: string, options: MultiselectOptions<T>): Promise<T[]>
118118
```
119119
120120
Scrollable multiselect depending `options.maxVisible` (default `8`).<br>
@@ -196,46 +196,46 @@ export interface AbstractPromptOptions {
196196
stdin?: Stdin;
197197
stdout?: Stdout;
198198
message: string;
199-
sginal?: AbortSignal;
199+
skip?: boolean;
200+
signal?: AbortSignal;
200201
}
201202

202-
export interface PromptValidator<T = string | string[] | boolean> {
203-
validate: (input: T) => boolean;
204-
error: (input: T) => string;
203+
export interface PromptValidator<T extends string | string[]> {
204+
validate: (input: T) => boolean;
205205
}
206206

207207
export interface QuestionOptions extends SharedOptions {
208208
defaultValue?: string;
209-
validators?: Validator[];
209+
validators?: PromptValidator<string>[];
210210
secure?: boolean;
211211
}
212212

213-
export interface Choice {
214-
value: any;
213+
export interface Choice<T = any> {
214+
value: T;
215215
label: string;
216216
description?: string;
217217
}
218218

219-
export interface SelectOptions extends SharedOptions {
220-
choices: (Choice | string)[];
219+
export interface SelectOptions<T extends string> extends AbstractPromptOptions {
220+
choices: (Choice<T> | T)[];
221221
maxVisible?: number;
222-
ignoreValues?: (string | number | boolean)[];
223-
validators?: Validator[];
222+
ignoreValues?: (T | number | boolean)[];
223+
validators?: PromptValidator<string>[];
224224
autocomplete?: boolean;
225225
caseSensitive?: boolean;
226226
}
227227

228-
export interface MultiselectOptions extends SharedOptions {
229-
choices: (Choice | string)[];
228+
export interface MultiselectOptions<T extends string> extends AbstractPromptOptions {
229+
choices: (Choice<T> | T)[];
230230
maxVisible?: number;
231-
preSelectedChoices?: (Choice | string)[];
232-
validators?: Validator[];
231+
preSelectedChoices?: (Choice<T> | T)[];
232+
validators?: PromptValidator<string[]>[];
233233
autocomplete?: boolean;
234234
caseSensitive?: boolean;
235235
showHint?: boolean;
236236
}
237237

238-
export interface ConfirmOptions extends SharedOptions {
238+
export interface ConfirmOptions extends AbstractPromptOptions {
239239
initial?: boolean;
240240
}
241241
```

package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
"scripts": {
66
"build": "tsup index.ts --format cjs,esm --dts --clean",
77
"prepublishOnly": "npm run build",
8-
"test": "glob -c \"tsx --test\" \"./test/**/*.test.ts\"",
9-
"coverage": "c8 -r html npm run test",
8+
"test-only": "glob -c \"tsx --test\" \"./test/**/*.test.ts\"",
9+
"test-types": "npm run build && tsd",
10+
"test": "c8 -r html npm run test-only && npm run test-types",
1011
"lint": "eslint src test",
1112
"lint:fix": "eslint . --fix"
1213
},
@@ -36,6 +37,7 @@
3637
"@types/node": "^24.0.3",
3738
"c8": "^10.1.3",
3839
"glob": "^11.0.0",
40+
"tsd": "^0.33.0",
3941
"tsup": "^8.3.5",
4042
"tsx": "^4.19.2",
4143
"typescript": "^5.7.2"
@@ -46,5 +48,8 @@
4648
"bugs": {
4749
"url": "https://github.com/TopCli/prompts/issues"
4850
},
49-
"homepage": "https://github.com/TopCli/prompts#readme"
51+
"homepage": "https://github.com/TopCli/prompts#readme",
52+
"tsd": {
53+
"directory": "test/types"
54+
}
5055
}

src/prompts/abstract.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export interface AbstractPromptOptions {
3030
signal?: AbortSignal;
3131
}
3232

33-
export class AbstractPrompt<T> extends EventEmitter {
33+
export class AbstractPrompt<T extends string | boolean> extends EventEmitter {
3434
stdin: Stdin;
3535
stdout: Stdout;
3636
message: string;

src/prompts/multiselect.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ const kRequiredChoiceProperties = ["label", "value"];
1515
export interface MultiselectOptions<T extends string> extends AbstractPromptOptions {
1616
choices: (Choice<T> | T)[];
1717
maxVisible?: number;
18-
preSelectedChoices?: (Choice | T)[];
19-
validators?: PromptValidator<T[]>[];
18+
preSelectedChoices?: (Choice<T> | T)[];
19+
validators?: PromptValidator<string[]>[];
2020
autocomplete?: boolean;
2121
caseSensitive?: boolean;
2222
showHint?: boolean;
@@ -27,7 +27,7 @@ type VoidFn = () => void;
2727
export class MultiselectPrompt<T extends string> extends AbstractPrompt<T> {
2828
#boundExitEvent: VoidFn = () => void 0;
2929
#boundKeyPressEvent: VoidFn = () => void 0;
30-
#validators: PromptValidator<T[]>[];
30+
#validators: PromptValidator<string[]>[];
3131
#showHint: boolean;
3232

3333
activeIndex = 0;
@@ -52,7 +52,11 @@ export class MultiselectPrompt<T extends string> extends AbstractPrompt<T> {
5252
return this.choices.filter((choice) => this.#filterChoice(choice, autocompleteValue, isCaseSensitive));
5353
}
5454

55-
#filterChoice(choice: T | Choice | string, autocompleteValue: string, isCaseSensitive = false) {
55+
#filterChoice(
56+
choice: T | Choice<T> | string,
57+
autocompleteValue: string,
58+
isCaseSensitive = false
59+
) {
5660
// eslint-disable-next-line no-nested-ternary
5761
const choiceValue = typeof choice === "string" ?
5862
(isCaseSensitive ? choice : choice.toLowerCase()) :

src/prompts/question.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { isValid, type PromptValidator, resultError } from "../validators.js";
1010

1111
export interface QuestionOptions extends AbstractPromptOptions {
1212
defaultValue?: string;
13-
validators?: PromptValidator[];
13+
validators?: PromptValidator<string>[];
1414
secure?: boolean | {
1515
placeholder: string;
1616
};
@@ -22,7 +22,7 @@ export class QuestionPrompt extends AbstractPrompt<string> {
2222
questionSuffixError: string;
2323
answer?: string;
2424
answerBuffer?: Promise<string>;
25-
#validators: PromptValidator[];
25+
#validators: PromptValidator<string>[];
2626
#secure: boolean;
2727
#securePlaceholder: string | null = null;
2828

src/prompts/select.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@ export interface SelectOptions<T extends string> extends AbstractPromptOptions {
1616
choices: (Choice<T> | T)[];
1717
maxVisible?: number;
1818
ignoreValues?: (T | number | boolean)[];
19-
validators?: PromptValidator<T>[];
19+
validators?: PromptValidator<string>[];
2020
autocomplete?: boolean;
2121
caseSensitive?: boolean;
2222
}
2323

2424
type VoidFn = () => void;
2525

26-
export class SelectPrompt<T extends string = string> extends AbstractPrompt<T> {
26+
export class SelectPrompt<T extends string> extends AbstractPrompt<T> {
2727
#boundExitEvent: VoidFn = () => void 0;
2828
#boundKeyPressEvent: VoidFn = () => void 0;
29-
#validators: PromptValidator<T>[];
29+
#validators: PromptValidator<string>[];
3030
activeIndex = 0;
3131
questionMessage: string;
3232
autocompleteValue = "";
@@ -48,7 +48,11 @@ export class SelectPrompt<T extends string = string> extends AbstractPrompt<T> {
4848
return this.choices.filter((choice) => this.#filterChoice(choice, autocompleteValue, isCaseSensitive));
4949
}
5050

51-
#filterChoice(choice: Choice | string, autocompleteValue: string, isCaseSensitive = false) {
51+
#filterChoice(
52+
choice: Choice<T> | string,
53+
autocompleteValue: string,
54+
isCaseSensitive = false
55+
) {
5256
// eslint-disable-next-line no-nested-ternary
5357
const choiceValue = typeof choice === "string" ?
5458
(isCaseSensitive ? choice : choice.toLowerCase()) :

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export interface Choice<T = any> {
1+
export interface Choice<T extends string> {
22
value: T;
33
label: string;
44
description?: string;

src/validators.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ export type ValidationResponse = InvalidResponse | ValidResponse;
1010
export type InvalidResponse = string | InvalidResponseObject;
1111
export type ValidResponse = null | undefined | true | ValidResponseObject;
1212

13-
export interface PromptValidator<T = string | string[] | boolean> {
13+
export interface PromptValidator<T extends string | string[]> {
1414
validate: (input: T) => ValidationResponse;
1515
}
1616

17-
export function required<T = string | string[] | boolean>(): PromptValidator<T> {
17+
export function required(): PromptValidator<any> {
1818
return {
1919
validate: (input) => {
2020
const isValid = (Array.isArray(input) ? input.length > 0 : Boolean(input));

test/types/api.test-d.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Import Third-party Dependencies
2+
import { expectType } from "tsd";
3+
4+
// Import Internal Dependencies
5+
import {
6+
question,
7+
confirm,
8+
select,
9+
multiselect,
10+
type PromptValidator
11+
} from "../../index.js";
12+
13+
const stringNotEmptyValidator: PromptValidator<string> = {
14+
validate(input) {
15+
return input.trim().length === 0 ?
16+
{ isValid: false, error: "Input was empty" } :
17+
{ isValid: true };
18+
}
19+
};
20+
21+
expectType<Promise<string>>(
22+
question("message", {
23+
validators: [stringNotEmptyValidator]
24+
})
25+
);
26+
27+
expectType<Promise<"A" | "B">>(
28+
select("message", { choices: ["A", "B"] })
29+
);
30+
expectType<Promise<"A" | "B">>(
31+
select("message", {
32+
choices: [
33+
{ value: "A", label: "Option A" },
34+
{ value: "B", label: "Option B" }
35+
]
36+
})
37+
);
38+
39+
expectType<Promise<("A" | "B")[]>>(
40+
multiselect("message", { choices: ["A", "B"] })
41+
);
42+
expectType<Promise<("A" | "B")[]>>(
43+
multiselect("message", {
44+
choices: [
45+
{ value: "A", label: "Option A" },
46+
{ value: "B", label: "Option B" }
47+
],
48+
preSelectedChoices: ["A"]
49+
})
50+
);
51+
52+
expectType<Promise<boolean>>(
53+
confirm("message")
54+
);
55+
expectType<Promise<boolean>>(
56+
confirm("message", { initial: true })
57+
);

0 commit comments

Comments
 (0)