-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
fix: marked and parse type overload with discrinated union options #3116
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
f407a72
to
f2b2219
Compare
f2b2219
to
1af8151
Compare
1af8151
to
76c9a43
Compare
src/Instance.ts
Outdated
|
||
// Show warning if an extension set async to true but the parse was called with async: false | ||
if (this.defaults.async === true && origOpt.async === false) { | ||
if (isAsyncOptions(this.defaults) && isSyncOptions(origOpt)) { | ||
if (!opt.silent) { | ||
console.warn('marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question for maintainer: could this warning not be silent ?
Because as far as i know #3103 tried to fix the type to avoid unexepected Promises when a library change the default.async to true
.
With this PR, the type returned by marked()
and marked.parse()
will have miss if a library change the default.async and user should have to be warned about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The silent option is for when they want things to fail instead of get warnings. If they want to see the error they can just set the silent option to false.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is what #3103 tried to fix. It looks like this PR breaks that again. I think we still want correct types even if people need to add as string
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well this PR doesn't break it pre see, it fixes the default typing based on default behavior (sending Promise if async
is set to true
).
So if the user want a fail instead of warning when a library modify the default, it's pretty ok ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So you are suggesting throwing an error here instead of logging a massage? I think we could do that. This would need to be a breaking change.
Has it as been said in #3103 there's no way to know if the default The idea of this PR is to have a known return type based on the passed configuration which #3103 kinda broke by forcing to user to use With this PR: (async() => {
const md = '# foobar';
const syncMarked: string = marked(md, { async: false }); // will work at compile time, but break at runtime if default has been changed
const asyncMarked: string = marked(md, { async: true }); // will break at compile time since return type will be `Promise<String>`
const syncMarkedParse: string = marked.parse(md, { async: false }); // will work at compile time, but break at runtime if default has been changed
const asyncMarkedParse: string = marked.parse(md, { async: true }); // will break at compile time since return type will be `Promise<String>`
const awaitedDefaultMarked: string = await marked(md); // will not break at compile time, will not break at runtime, even if default has been changed
const awaitedDefaultMarkedParse: string = await marked.parse(md); // will not break at compile time, will not break at runtime, even if default has been changed
})(); Maybe when changing type to: function marked(src: string, options: MarkedAsyncOptions): Promise<string>;
function marked(src: string, options: MarkedSyncOptions): string;
function marked(src: string, options?: MarkedOptions & { async?: undefined }): Promise<string> | string;
//or
function marked(src: string, options?: Exclude<MarkedOptions, 'async'>): Promise<string> | string; But that mean introducing a breaking change here: - if (isAsyncOptions(this.defaults) && isSyncOptions(origOpt)) {
+ if (isAsyncOptions(this.defaults) && typeof origOpt.async === 'undefined') { Which mean, fallback to default option, even overloaded by library, only if the developper does not have explicitly asked for an expected behavior. |
Before #3103 and if we merge this PR the type retuned be marked.parse could be wrong. If you are ok with not knowing the correct return type, just don't use typescript. What is the difference from a user adding |
The second one will break at runtime if a library change the default config and dev not knowing it will don't understand why since they force casted a type to the output. |
The first one will also break at runtime with this PR and before #3103 |
So I think the only solution to not break at runtime if the developer doesn't know if they added an async extension is to leave it the way it is and await marked.parse |
If the developer does know that they didn't add an async extension than |
True that, that's why suggested adding a breaking change in my previous comment, making the behavior more explicit: I don't know what impact it will have for library using marked, but since it's a breaking change, this will change the major version of marked, this way preventing update for user prefixing their package.json version with at least Edit: i'm eager to create a new PR for that, including documentation update, since it was also a point in #3103 by this comment #3103 (comment) about two entry points. |
Can you please merge this? Having .parse return For the time being, if anyone’s as bothered as I am, here’s a helper method you can use: import { MarkedOptions, marked } from "marked";
export function parseMarkdown(markdown: string, options: Omit<MarkedOptions, 'async'> = {}) {
const parsed = marked.parse(markdown, options);
if ( typeof parsed === 'string' ) return parsed;
throw new Error(`Expected marked to return a string, but it returned ${parsed}`);
}; |
If this was complete we could merge it in the next major version. A couple of things that need to be fixed:
|
I'll try to find some time to fix it as soon as possible 👍 |
76c9a43
to
1e602f2
Compare
@UziTech added a new commit that replace the warning with a throw, and adding tests reflecting this changes. |
src/Instance.ts
Outdated
if (!opt.silent) { | ||
console.warn('marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored.'); | ||
throw new Error('marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use the throwError
function instead of throwing the error. Similar to other errors in this function. You will have the move the creation of the function above this if statement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the hiatus, just fixed this point, for the second point i'll try to figure out how to do it, since it need to modify typing in the .use()
function to output the correct instance type.
const defaultMarked: string = marked(md); | ||
// as string can be used if no extensions set `async: true` | ||
const stringMarked: string = marked(md) as string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Marked calls that don't specify an async option should be string | Promise<string>
since they could be either and they won't throw an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since they could be either
Just curious, how’s that? If no async
is specified, it defaults to false
, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If no async is specified it should default to whatever async is set by extensions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const syncMarked = new Marked({ async: false });
syncMarked.parse(md) // this will be a string
const asyncMarked = new Marked({ async: true });
asyncMarked.parse(md) // this will be a Promise<string>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe there's a way to have typed returned instance ?
the default instance, without any modification, will be synch, right ?
const marked = new Marked();
marked.parse(md) // this will be a string if global config has not be mutated by an external library
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe. Typescript would have to run your application to see what the return type is.
Yes default without any extensions is sync
src/Instance.ts
Outdated
opt.async = true; | ||
if (isAsyncOptions(this.defaults) && isSyncOptions(origOpt)) { | ||
// Throw an error if an extension set async to true but the parse was called with async: false | ||
return throwError(new Error('marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored.')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message should be:
marked(): The async option was set to true by an extension. Remove the async: false option to continue.
src/MarkedOptions.ts
Outdated
} | ||
|
||
export function isSyncOptions(options: MarkedOptions): options is MarkedSyncOptions { | ||
return options.async === false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be false
or undefined
? Or maybe != true
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pushed something like this:
return !isAsyncOptions(options);
It fail a lot of tests since it introduce a breaking change, i have to dive in to understand why.
…ith async at false BREAKING CHANGE: parser now throw an error when `defaultConfig` is modified by an extension to make the returned value a Promise and the we call parser with `async` option at false. It can be skipped with `silent` to true, making it still working for library that use it to modify the config globally but still calling each instance with the option `async` at false.
3e7320a
to
66199ad
Compare
Marked version: 11.0.0
Description
Fixes typescript expected error which force to use
as string
oras Promise
orts-expect-error
comments by using disciminated union for options.Added two type guards to detect which options object is used.
Expectation
as string
oras Promise<string>
can be safely removed when usingmarked()
andmarked.parse()
with respective config objectResult
No regression, no behavior change
Contributor
Committer
In most cases, this should be a different person than the contributor.