Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve type of the std*: AsyncGeneratorFunction option #694

Open
ehmicky opened this issue Jan 16, 2024 · 0 comments
Open

Improve type of the std*: AsyncGeneratorFunction option #694

ehmicky opened this issue Jan 16, 2024 · 0 comments

Comments

@ehmicky
Copy link
Collaborator

ehmicky commented Jan 16, 2024

See #693 (comment)

Object mode

The stdin/stdout/stderr/stdio option can be a generator function.

const transform = async function * (chunk) {
	yield chunk.toUpperCase();
};

const {stdout} = await execa('echo', ['hello'], {stdout: transform});

The chunk is currently typed as unknown and the return value is an [Async]Generator<unknown, void, void>. Additionally, the input option is also an Iterable<Unknown> or AsyncIterable<unknown>.

That's because when the stream is in object mode, the chunk type can be anything. However, when not in object mode, it is string | Uint8Array. It is technically feasible (but hard) to determine with our types whether object mode is used, and type accordingly. The following needs to be considered:

  • The stream can be set with the stdin/stdout/stderr options, but also the stdio[0|1|2] option
  • The stdio option can have additional items beyond those 3 streams
  • Those options can either be a single value or an array of values
  • Transforms are in object mode when using { transform, objectMode: true }
  • The stream is in object mode when its transforms are in object mode
  • The stream might mix transforms in object mode and not
    • Output streams are in object mode if their last transform is in object mode
    • Input streams are in object mode if their first transform is in object mode
    • stdin is always an input stream, stdout/stderr are output streams. stdio[3] and beyond are usually output streams, unless one the stdio[*] option uses an iterable, Uint8Array, Readable, ReadableStream, 0 or process.stdin.
  • The stream is not in object mode when it has no transforms
  • The stdin/stdout/stderr/stdio options' value can be an array of both transforms and non-transforms. The non-transforms should be ignored for the logic above.
  • In practice, we could use a shortcut where the type consider a stream in object mode if it uses any transform in object mode. That's because mixing multiple transforms in both object mode and not is uncommon and not always useful.
  • The input option uses stdin's object mode

The above looks scary, but we actually already have this logic with our types.

execa/index.d.ts

Lines 102 to 135 in 16e5fd6

// Whether `result.stdout|stderr|all` is an array of values due to `objectMode: true`
type IsObjectStream<
StreamIndex extends string,
OptionsType extends CommonOptions = CommonOptions,
> = IsObjectNormalStream<StreamIndex, OptionsType> extends true
? true
: IsObjectStdioStream<StreamIndex, OptionsType['stdio']>;
type IsObjectNormalStream<
StreamIndex extends string,
OptionsType extends CommonOptions = CommonOptions,
> = IsObjectOutputOptions<StreamOption<StreamIndex, OptionsType>>;
type IsObjectStdioStream<
StreamIndex extends string,
StdioOptionType extends StdioOptions | undefined,
> = StdioOptionType extends StdioOptionsArray
? StreamIndex extends keyof StdioOptionType
? StdioOptionType[StreamIndex] extends StdioOption
? IsObjectOutputOptions<StdioOptionType[StreamIndex]>
: false
: false
: false;
type IsObjectOutputOptions<OutputOptions extends StdioOption> = IsObjectOutputOption<OutputOptions extends StdioSingleOption[]
? OutputOptions[number]
: OutputOptions
>;
type IsObjectOutputOption<OutputOption extends StdioSingleOption> = OutputOption extends StdioTransformFull
? BooleanObjectMode<OutputOption['objectMode']>
: false;
type BooleanObjectMode<ObjectModeOption extends StdioTransformFull['objectMode']> = ObjectModeOption extends true ? true : false;

The hard part is actually different. The main issue is that we need to type the options.std* arguments, while at the same time infer types from that same argument. We could use an union to declare that each option can use either only unknown but no non-object mode transforms, or only string | Uint8Array but no object mode transforms.

All of the above also impacts the final method. Although it does not have any argument, its return type is the same as the transform method.

Encoding

Once this is done, we can make types even stricter by typing those as either Uint8Array or string (when not in object mode) instead of Uint8Array | string. This is decided by whether either the encoding option is binary, or whether the binary option is set.

Please note the return value of the transform and final methods is always Uint8Array | string, even when the argument is only string or only Uint8Array.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant