diff --git a/README.md b/README.md index 42c7e74..574eaf8 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ const name = await question("What's your name ?", { ### `select()` ```ts -select(message: string, options: SelectOptions): Promise +select(message: string, options: SelectOptions): Promise ``` Scrollable select depending `maxVisible` (default `8`). @@ -114,7 +114,7 @@ Use `options.skip` to skip prompt. It will return the first choice. ### `multiselect()` ```ts -multiselect(message: string, options: MultiselectOptions): Promise<[string]> +multiselect(message: string, options: MultiselectOptions): Promise ``` Scrollable multiselect depending `options.maxVisible` (default `8`).
@@ -196,46 +196,46 @@ export interface AbstractPromptOptions { stdin?: Stdin; stdout?: Stdout; message: string; - sginal?: AbortSignal; + skip?: boolean; + signal?: AbortSignal; } -export interface PromptValidator { - validate: (input: T) => boolean; - error: (input: T) => string; +export interface PromptValidator { + validate: (input: T) => boolean; } export interface QuestionOptions extends SharedOptions { defaultValue?: string; - validators?: Validator[]; + validators?: PromptValidator[]; secure?: boolean; } -export interface Choice { - value: any; +export interface Choice { + value: T; label: string; description?: string; } -export interface SelectOptions extends SharedOptions { - choices: (Choice | string)[]; +export interface SelectOptions extends AbstractPromptOptions { + choices: (Choice | T)[]; maxVisible?: number; - ignoreValues?: (string | number | boolean)[]; - validators?: Validator[]; + ignoreValues?: (T | number | boolean)[]; + validators?: PromptValidator[]; autocomplete?: boolean; caseSensitive?: boolean; } -export interface MultiselectOptions extends SharedOptions { - choices: (Choice | string)[]; +export interface MultiselectOptions extends AbstractPromptOptions { + choices: (Choice | T)[]; maxVisible?: number; - preSelectedChoices?: (Choice | string)[]; - validators?: Validator[]; + preSelectedChoices?: (Choice | T)[]; + validators?: PromptValidator[]; autocomplete?: boolean; caseSensitive?: boolean; showHint?: boolean; } -export interface ConfirmOptions extends SharedOptions { +export interface ConfirmOptions extends AbstractPromptOptions { initial?: boolean; } ``` diff --git a/package.json b/package.json index a3a0dfd..cbbf613 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,9 @@ "scripts": { "build": "tsup index.ts --format cjs,esm --dts --clean", "prepublishOnly": "npm run build", - "test": "glob -c \"tsx --test\" \"./test/**/*.test.ts\"", - "coverage": "c8 -r html npm run test", + "test-only": "glob -c \"tsx --test\" \"./test/**/*.test.ts\"", + "test-types": "npm run build && tsd", + "test": "c8 -r html npm run test-only && npm run test-types", "lint": "eslint src test", "lint:fix": "eslint . --fix" }, @@ -36,6 +37,7 @@ "@types/node": "^24.0.3", "c8": "^10.1.3", "glob": "^11.0.0", + "tsd": "^0.33.0", "tsup": "^8.3.5", "tsx": "^4.19.2", "typescript": "^5.7.2" @@ -46,5 +48,8 @@ "bugs": { "url": "https://github.com/TopCli/prompts/issues" }, - "homepage": "https://github.com/TopCli/prompts#readme" + "homepage": "https://github.com/TopCli/prompts#readme", + "tsd": { + "directory": "test/types" + } } diff --git a/src/prompts/abstract.ts b/src/prompts/abstract.ts index 79e8708..e241470 100644 --- a/src/prompts/abstract.ts +++ b/src/prompts/abstract.ts @@ -30,7 +30,7 @@ export interface AbstractPromptOptions { signal?: AbortSignal; } -export class AbstractPrompt extends EventEmitter { +export class AbstractPrompt extends EventEmitter { stdin: Stdin; stdout: Stdout; message: string; diff --git a/src/prompts/multiselect.ts b/src/prompts/multiselect.ts index 92d0816..afb4ee2 100644 --- a/src/prompts/multiselect.ts +++ b/src/prompts/multiselect.ts @@ -15,8 +15,8 @@ const kRequiredChoiceProperties = ["label", "value"]; export interface MultiselectOptions extends AbstractPromptOptions { choices: (Choice | T)[]; maxVisible?: number; - preSelectedChoices?: (Choice | T)[]; - validators?: PromptValidator[]; + preSelectedChoices?: (Choice | T)[]; + validators?: PromptValidator[]; autocomplete?: boolean; caseSensitive?: boolean; showHint?: boolean; @@ -27,7 +27,7 @@ type VoidFn = () => void; export class MultiselectPrompt extends AbstractPrompt { #boundExitEvent: VoidFn = () => void 0; #boundKeyPressEvent: VoidFn = () => void 0; - #validators: PromptValidator[]; + #validators: PromptValidator[]; #showHint: boolean; activeIndex = 0; @@ -52,7 +52,11 @@ export class MultiselectPrompt extends AbstractPrompt { return this.choices.filter((choice) => this.#filterChoice(choice, autocompleteValue, isCaseSensitive)); } - #filterChoice(choice: T | Choice | string, autocompleteValue: string, isCaseSensitive = false) { + #filterChoice( + choice: T | Choice | string, + autocompleteValue: string, + isCaseSensitive = false + ) { // eslint-disable-next-line no-nested-ternary const choiceValue = typeof choice === "string" ? (isCaseSensitive ? choice : choice.toLowerCase()) : diff --git a/src/prompts/question.ts b/src/prompts/question.ts index da7aed0..791eece 100644 --- a/src/prompts/question.ts +++ b/src/prompts/question.ts @@ -10,7 +10,7 @@ import { isValid, type PromptValidator, resultError } from "../validators.js"; export interface QuestionOptions extends AbstractPromptOptions { defaultValue?: string; - validators?: PromptValidator[]; + validators?: PromptValidator[]; secure?: boolean | { placeholder: string; }; @@ -22,7 +22,7 @@ export class QuestionPrompt extends AbstractPrompt { questionSuffixError: string; answer?: string; answerBuffer?: Promise; - #validators: PromptValidator[]; + #validators: PromptValidator[]; #secure: boolean; #securePlaceholder: string | null = null; diff --git a/src/prompts/select.ts b/src/prompts/select.ts index f274187..3a3a22b 100644 --- a/src/prompts/select.ts +++ b/src/prompts/select.ts @@ -16,17 +16,17 @@ export interface SelectOptions extends AbstractPromptOptions { choices: (Choice | T)[]; maxVisible?: number; ignoreValues?: (T | number | boolean)[]; - validators?: PromptValidator[]; + validators?: PromptValidator[]; autocomplete?: boolean; caseSensitive?: boolean; } type VoidFn = () => void; -export class SelectPrompt extends AbstractPrompt { +export class SelectPrompt extends AbstractPrompt { #boundExitEvent: VoidFn = () => void 0; #boundKeyPressEvent: VoidFn = () => void 0; - #validators: PromptValidator[]; + #validators: PromptValidator[]; activeIndex = 0; questionMessage: string; autocompleteValue = ""; @@ -48,7 +48,11 @@ export class SelectPrompt extends AbstractPrompt { return this.choices.filter((choice) => this.#filterChoice(choice, autocompleteValue, isCaseSensitive)); } - #filterChoice(choice: Choice | string, autocompleteValue: string, isCaseSensitive = false) { + #filterChoice( + choice: Choice | string, + autocompleteValue: string, + isCaseSensitive = false + ) { // eslint-disable-next-line no-nested-ternary const choiceValue = typeof choice === "string" ? (isCaseSensitive ? choice : choice.toLowerCase()) : diff --git a/src/types.ts b/src/types.ts index 176c755..0070a2b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -export interface Choice { +export interface Choice { value: T; label: string; description?: string; diff --git a/src/validators.ts b/src/validators.ts index fc3eaea..d889d1d 100644 --- a/src/validators.ts +++ b/src/validators.ts @@ -10,11 +10,11 @@ export type ValidationResponse = InvalidResponse | ValidResponse; export type InvalidResponse = string | InvalidResponseObject; export type ValidResponse = null | undefined | true | ValidResponseObject; -export interface PromptValidator { +export interface PromptValidator { validate: (input: T) => ValidationResponse; } -export function required(): PromptValidator { +export function required(): PromptValidator { return { validate: (input) => { const isValid = (Array.isArray(input) ? input.length > 0 : Boolean(input)); diff --git a/test/types/api.test-d.ts b/test/types/api.test-d.ts new file mode 100644 index 0000000..cc24989 --- /dev/null +++ b/test/types/api.test-d.ts @@ -0,0 +1,57 @@ +// Import Third-party Dependencies +import { expectType } from "tsd"; + +// Import Internal Dependencies +import { + question, + confirm, + select, + multiselect, + type PromptValidator +} from "../../index.js"; + +const stringNotEmptyValidator: PromptValidator = { + validate(input) { + return input.trim().length === 0 ? + { isValid: false, error: "Input was empty" } : + { isValid: true }; + } +}; + +expectType>( + question("message", { + validators: [stringNotEmptyValidator] + }) +); + +expectType>( + select("message", { choices: ["A", "B"] }) +); +expectType>( + select("message", { + choices: [ + { value: "A", label: "Option A" }, + { value: "B", label: "Option B" } + ] + }) +); + +expectType>( + multiselect("message", { choices: ["A", "B"] }) +); +expectType>( + multiselect("message", { + choices: [ + { value: "A", label: "Option A" }, + { value: "B", label: "Option B" } + ], + preSelectedChoices: ["A"] + }) +); + +expectType>( + confirm("message") +); +expectType>( + confirm("message", { initial: true }) +);