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

async/await: nowait keyword? #13376

Open
kduffie opened this issue Jan 10, 2017 · 92 comments
Open

async/await: nowait keyword? #13376

kduffie opened this issue Jan 10, 2017 · 92 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@kduffie
Copy link

kduffie commented Jan 10, 2017

I have a large project that has now been migrated over from javascript to typescript and we're very pleased with the results. Bugs have fallen dramatically with the improved compiler support in combination with rigorously written code.

Ours is a server-side product that runs under node (ts-node, actually). By its nature, it is highly asynchronous, so the move to Typescript had the major benefit of providing us access to async/await. It has been working really well, and dramatically reduced our code size and complexity.

The one problem it has introduced is that we occasionally miss putting an "await" keyword in front of calls to methods returning promises (i.e., other async functions) when inside an async function. This "send and forget" is something that one occasionally might want to use, but for the most part, this creates a lot of chaos in our design pattern, as in most cases, the call is to something that will complete asynchronously, but most of the time we need to wait for that to complete. (That method cannot be synchronous because it depends on external services that are asynchronous.) It is not uncommon in our case to have 5 or 10 await statements in a single method.

A missing "await" keyword can be hard to find -- especially when the method being called doesn't return anything that is needed by the caller. (If it returns something, type-checking will usually catch the mismatch between the type of the Promise returned from the call.)

Ideally, we could have a "nowait" keyword that would go where one would otherwise put an "await" to indicate to the Typescript compiler that the design intent is to explicitly NOT WAIT for the called method to be completing. The tsc compiler could have a new flag that controls whether to create a warning if a method call returns a Promise that is missing either "await" or "nowait". The "nowait" keyword would have no effect whatsoever on the compiled code.

@aseemk
Copy link

aseemk commented Jan 16, 2017

I strongly agree with the problem this issue is trying to address. My colleagues and I continue to hit this all the time. (I'd estimate at least once a week.) And it frequently masks bugs.

It goes without saying that this only happens on void methods. But a lot of times, those void methods are significant, e.g. validateFoo, checkAccess, async tests, etc.

I imagine the specific solution proposed in this issue (of a nowait keyword) will be challenging since async/await is an ES7 feature, not a TypeScript one. But here are other related issues:

#10381
palantir/tslint#892

I'd be a fan of having this check built into the compiler rather than the linter, since it's truly a correctness issue, not a style issue.

Thanks to everyone for the consideration!

@alitaheri
Copy link

I have recently started migrating a huge project from promises to async/await. having this feature can really ease such migrations. It's very hard to detect dangling promises until something goes terribly wrong with hours of trying to reproduce the issue only to find a function call was missing await 😱

@aluanhaddad
Copy link
Contributor

This would be highly valuable but the problem with a noawait keyword is that it introduces new expression level syntax.

However, we can avoid this problem by taking the idea and introducing a flag, --requireAwait (:bike:🏠), which would trigger an error if a top level expression of type Promise<T> within an async method is not awaited and its value is not part of an expression or an assignment.

To suppress the error, the language could require a type assertion, which would otherwise be meaningless

declare function f(): Promise<void>;

// --requireAwait
async function g() {
  f(); // Error
  f() as {}; // OK
  await f(); // OK
  const p = f(); // OK
  p; // Error
  p as {}; // OK
  Promise.all([f(), p]); // Error because of .all
  await Promise.all([f(), p]); // OK
  return f(); // OK
}

There are some issues with this idea though.

  1. It adds a new flag
  2. const p = f() is currently valid but likely indicates an error so encouraging it could be bad
  3. Type assertions look weird in unused expressions.
  4. Stylistically, refactoring the Promise<void> to return a value is generally preferable(but sometimes not possible or desirable).
  5. Probably a number of things I have not considered.

@alitaheri
Copy link

alitaheri commented Jan 17, 2017

@aluanhaddad I like that idea.

Since typescript is going to introduce decorators like @@deprecated how about @@requireAwait? Might even be able to provide an implementation that actually catches these in run time. No more flags too 😁
It will be opt-in.

Edit: or maybe opt-out with a flag: --requireAwait and @@ignoreAwait

@aluanhaddad
Copy link
Contributor

@alitaheri that would be much cleaner. Did you mean @@deprecated?

@alitaheri
Copy link

yes, I had forgotten that it was a bit different. Thanks for the reminder 😅 😅

@aseemk
Copy link

aseemk commented Jan 17, 2017

@aluanhaddad: I love your proposal. My colleague @ryanwe has had a similar thought: this issue can be caught when a caller doesn't make use of a returned Promise.

I don't feel like the issues you list are a big deal. To me, it's a major step forward for the compiler to help guard against the 90% bug case, even if it doesn't address the last 10%. (The linter can help w/ that last 10%, e.g. const p = f() and you don't make use of that p.)

@alitaheri: I might not understand your proposal. Where would I put this @@requireAwait decorator? On every single async method I have? (I see your edit now for flipping that to @@ignoreAwait 👍 )

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Jan 17, 2017
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jan 17, 2017

There's no need for nowait - the void operator already exists for a consuming an expression while producing no side effects (e.g. you could just write void doSomethingAsync();)

@kduffie
Copy link
Author

kduffie commented Jan 17, 2017

@RyanCavanaugh I think you are missing a key point. The problem is not about whether the promise gets executed. The problem is that the writer forgets to put the "await" in front of a call to an async method/function and gets a behavior that he/she is not expecting. So I believe this is a compiler warning/error issue only -- not a functional one. I'd be fine if this was "opt-in" in the sense that one would have to choose to turn on a compiler option to get warnings about calls to methods returning promises inside async functions that are unused. If using this compiler option, the 'nowait' keyword could be used if the writer has the (rare) need to invoke the method but not wait for the result.

@aluanhaddad
Copy link
Contributor

@kduffie I think what @RyanCavanaugh is saying is that a flag is sufficient and that no new syntax is required. That is a great thing. I don't think he is dismissing the issue as it is now marked as in discussion.

@aluanhaddad
Copy link
Contributor

The funny thing my initial type level syntax suggestion, was going to involve asserting that the type of the expression was void

async function f() {
  returnsPromise() as void;
}

which is a type error.
It never occurred to me to use

async function f() {
  void returnsPromise();
}

Which is far more elegant and is a JavaScript construct 😅

@kduffie
Copy link
Author

kduffie commented Jan 17, 2017

Ah. I see. Sorry for the confusion. He's saying that "void" would tell the compiler that you understood that promise returned is intentionally unused. With the new compiler flag present, one would get an error if there is neither an await nor a "void". Got it. Just fine with me.

@aluanhaddad
Copy link
Contributor

@kduffie I believe so. I think that would be a practical solution, but we will see.

@RyanCavanaugh
Copy link
Member

Chatted with @rbuckton about this and his take was that we could enforce this rule in async functions only with a flag without much trouble. Certainly the value proposition is clear - this stuff sounds like a nightmare to debug.

The "in async" would be needed because innocuous code like this (in a non-async function):

getSomePromise().then(() => whatver());

would be incorrectly flagged as an error because then itself returns a Promise.

@RyanCavanaugh
Copy link
Member

And yes as usual @aluanhaddad has cleared up my terse comments with precision and insight

@RyanCavanaugh RyanCavanaugh added Help Wanted You can do this Committed The team has roadmapped this issue and removed In Discussion Not yet reached consensus labels Jan 24, 2017
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jan 24, 2017

Approved as the default behavior (i.e. no flag):

  • It is an error in an async function for the expression of an expression statement to be exactly a function call or method call whose return type is Promise

Workarounds would be e.g. void f(); or <any>o.m(); for intentionally-discarded Promises.

If anyone thinks there are good examples of functions that return Promises where this would be burdensome, please speak up now with specifics!

Edit: Improved clarity of rule

@zpdDG4gta8XKpMCd
Copy link

can believe its for spproved for asyncs/promises only

@jwbay
Copy link
Contributor

jwbay commented Jan 24, 2017

I think this is a fairly common pattern for firing off a bunch of async work at once and then awaiting in one spot.

declare function fetchUser(): Promise<any>;
declare function fetchAccount(): Promise<any>;

async function loadStuff() {
	const userPromise = fetchUser();
	const accountPromise = fetchAccount();
	const [user, account] = await Promise.all([userPromise, accountPromise]);
	//do stuff
}

@mhegazy
Copy link
Contributor

mhegazy commented Jan 24, 2017

the example @jwbay mentioned would still be allowed. the ones that would be flagged as errors are expression statements. e.g.

async function loadStuff() {
     fetchUser();  // error, did you forget await?

    const accountPromise = fetchAccount();   // OK

}

@kduffie
Copy link
Author

kduffie commented Jan 26, 2017

I tried the new no-floating-promises rule in tslint 4.4 and it works great!

EXCEPT that I'm using VisualStudio code and its tslint integration won't show this error because this rule requires type checking and it appears that that isn't supported.

Anyone know if there are plans to fix VSCode so that it can handle type checking rules?

@RyanCavanaugh
Copy link
Member

@kduffie we're working on an extensibility model that will allow TSLint to report errors in editors
image

@kduffie
Copy link
Author

kduffie commented Jan 26, 2017

tslint already shows errors in the Problems window in vscode (using the tslint extension) but I'm now seeing the following -- which I assume is a limitation of the tslint extension.

Is that what you mean when you say, "... an extensibility model"?

vscode-tslint: 'no-floating-promises requires type checking' while validating: /Users/kduffie/git/kai/ts_modules/task-helper.ts
stacktrace: Error: no-floating-promises requires type checking
at Rule.TypedRule.apply (/Users/kduffie/git/kai/node_modules/tslint/lib/language/rule/typedRule.js:34:15)
at Linter.applyRule (/Users/kduffie/git/kai/node_modules/tslint/lib/linter.js:138:33)
at Linter.lint (/Users/kduffie/git/kai/node_modules/tslint/lib/linter.js:104:41)
at doValidate (/Users/kduffie/.vscode/extensions/eg2.tslint-0.8.1/server/server.js:369:20)
at validateTextDocument (/Users/kduffie/.vscode/extensions/eg2.tslint-0.8.1/server/server.js:285:27)
at documents.forEach.err (/Users/kduffie/.vscode/extensions/eg2.tslint-0.8.1/server/server.js:274:13)
at Array.forEach (native)
at validateAllTextDocuments (/Users/kduffie/.vscode/extensions/eg2.tslint-0.8.1/server/server.js:272:15)
at connection.onDidChangeWatchedFiles (/Users/kduffie/.vscode/extensions/eg2.tslint-0.8.1/server/server.js:523:9)

@Roam-Cooper
Copy link

I asked for this ages ago and was told to use a linter for it instead...

At the end of my issue I said "linting works for us", but that was a lie.

How can I use a linter for it when the linter cannot tell that the return type of an arbitrary functions is a promise or not, whereas the typescript compiler can?

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 6, 2023

I just tried implementing a simple version of this to see what breaks, and I'm not quite clear on what rule is both correct and consistent.

I started with the rule "A statement expression that is not an assignment expression must not have a type that is an instance of the global Promise type", which seems to be the sort of error people are making.

That does the right thing in the straightforward case:

    declare function doThing(): Promise<string>;
    
    async function f() {
        // Should error
        doThing();
        ~~~~~~~~~~
!!! error TS7062: Unawaited Promise
    
        let m: Promise<string>;
        // Should not error
        m = doThing();
    
        // Should not error
        void doThing();
    }

However, Promise.#then also returns a Promise, so it appears as if this code is also wrong:

declare function bar(): Promise<string>;
async function foo() {
    // Queue up some work
    bar().then(() => {
        console.log("Job's done");
    });
}

Other code also appears wrong despite being fairly idiomatic, e.g. this is technically an unawaited promise:

async function foo() {
    (async function() {
        // Do some deferred work
    })();
}

What have people mostly run into? Is the idea to just restrict this check to function and method calls occurring at the top level of a statement (unless the method is a method of Promise)? I'm trying see a rule that isn't a tangly mess of special cases but not coming up with much.

@jpike88
Copy link

jpike88 commented Mar 6, 2023

The biggest problem I think to address is when developers execute a promise (whether intentionally or accidentally), but forget to await it and do something with the results. Skimming through code it's easy to miss an un-awaited promise, as it can be easily mistaken for a synchronous operation.

So in your examples, simply adding the void operator would do just fine. There will be situations (rare in my experience) where you'll want to execute a promise on purpose without trying to resolve it to a result. So I think just being able to enforce a choice between void or await would do the trick. That way, if someone wrote void before something you can be confident the writer intended for the promise to run that way.

P.S. I would go a little further with this: a promise shouldn't be used in a truthy context as it's almost certainly a mistake and the coder probably intended for it to resolve to a value:

const promise = Promise.resolve('value');

if (promise) {   // this should be considered a problem
  ...
}

while(promise) { // problem...
 ...
}

That would cover all the common pitfalls we have using promises in async functions.

@kduffie
Copy link
Author

kduffie commented Mar 6, 2023

We have a large Typescript nodejs project and depend heavily on these ESLINT rules to catch errors. The most common errors happen when an "await" is forgotten in more complex statements, such as within predicate clauses of conditional statements. But without these checks, we would occasionally just fail to include an await on a simple statement invoking an asynchronous function or method. We use "void" instead of "await" whenever we intentionally want a separate "thread" to continue without waiting for it to complete. That convention has worked really well for us.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 6, 2023

Are people really wanting an error on this block, though? It seems very superfluous to have to use void here; the code is not plausibly wrong. Anyone have a codebase with the style check enabled on that I could look at?

async function foo() {
    // Queue up some work
    bar().then(() => {
        // do some other stuff
        console.log("Job's done");
    });
    // ^ error, floating promise created by `then` call
}

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 6, 2023

P.S. I would go a little further with this: a promise shouldn't be used in a truthy context as it's almost certainly a mistake and the coder probably intended for it to resolve to a value:

@jpike88 This already is an error. Can you clarify?

@kduffie
Copy link
Author

kduffie commented Mar 6, 2023

@RyanCavanaugh It seems perverse to mark the function as "async" and then use this construct. The whole point of an async function is to allow you to write it as:

async function foo() {
   await bar();
   console.log("Job's done");
}

For that reason, if you actually write bar().then(...); I would expect you to have to add "void" in front to show that you intended NOT to use the promise returned from that then() call.

I agree with you that a promise should never be implicitly used as truthy. You can always be explicit, ie., if (Boolean(p)) rather than if (p).

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 6, 2023

It seems perverse to mark the function as "async" and then use this construct.

But are unused Promises in non-async functions OK, then? It seems weird that we would say that this is an error

async function foo() {
   doSomeSetup(); // <- ⚠️actually async!
   doSomethingThatDependsOnSetup();
}

but not this

function foo() {
   doSomeSetup(); // <- ⚠️actually async!
   doSomethingThatDependsOnSetup();
}

when the motivating scenario for the feature is the "didn't realize the function was async" failure mode, which is just as much present in non-async functions.

@kduffie
Copy link
Author

kduffie commented Mar 6, 2023

That's true. In our situation, almost every method/function is async. The two most common mistakes are forgetting to add the async keyword to a function/method declaration or to forget to add the await keyword in front of a method/function call (whether that is part of a compound statement or not. For us, these are the most critical to detect.

So for me, in either case (async function or not), I would want it to be an error to invoke a method that returns a promise without using either "void" or "await" in front of it. (I acknowledge that the use of "void" is just syntactic sugar, but the whole point of Typescript is to allow the syntax to be rigorous.)

@emilioplatzer
Copy link

Are people really wanting an error on this block, though? It seems very superfluous to have to use void here; the code is not plausibly wrong. Anyone have a codebase with the style check enabled on that I could look at?

async function foo() {
    // Queue up some work
    bar().then(() => {
        // do some other stuff
        console.log("Job's done");
    });
    // ^ error, floating promise created by `then` call
}

Hi Ryan. I'm very happy with you getting this.

Yes, I already have a lot of old code written in that way. 99% of the times when I write

async function foo() {
    // Queue up some work
    bar().then(() => {
        // do some other stuff
        console.log("Job's done");
    });
    // ^ error, floating promise created by `then` call
}

I forgot the return in the front of bar().then(() => {.

In less of 1% of the cases I realy want a floating promise.

Then I prefer that the TS compiler always complain about unasigned o unreturned bar().then, also complain about (async ()=>{ blah(); })().

@emilioplatzer
Copy link

In my opinion the complain about floating promise must be inside async and not async functions and functions that returns Promises and function that returns any other type or functions that returns void. I.E. all functions.

@jpike88
Copy link

jpike88 commented Mar 7, 2023

Are people really wanting an error on this block, though? It seems very superfluous to have to use void here

It provides consistency to the code. If a promise can have either nothing, void, return await in front of it, and it's being able to just have nothing that creates ambiguity. At least with void/return/await, the promise is always explicit about the way in which it's intended to be used, aligning with the way in which it's actually going to run. This is priceless when deciphering code written by others, even yourself!

@jpike88 This already is an error. Can you clarify?

The below code doesn't flag anything in vscode, this is what I mean by a promise being used in an if statement without being awaited or voided.

const promise = new Promise<void>((resolve) => { resolve() });

if (promise) { // this should be an error saying to add await or void
      console.log('test')
}

const val = promise ? 123 : 456; // this should be an error saying to add await or void

while (promise) { // this should be an error saying to add await or void
   console.log('test')
}

@emilioplatzer
Copy link

In my opinion this can be achieved with [[nodiscard]] C++ like solution if we add [[nodiscard]] to the type Promise.

The [[nodiscard]] is reopened: #8240

@jpike88
Copy link

jpike88 commented Mar 7, 2023

@emilioplatzer I disagree, nodiscard if I'm not mistaken sounds like an additional, optional decorator that a library author would use to provide a clue as to how the method is supposed to be used.

I am for a core type checking rule that assumes that all Promise/async calls are assumed to be await-ed or is returned from a function. In other words, floating promises are treated as an anti-pattern or a mistake, and if they're being floated on purpose, the void operator can be used to 'silence' the error as well as serve as a visual marker to other coders that the promise is being floated on purpose.

@emilioplatzer
Copy link

emilioplatzer commented Mar 7, 2023

@jpike88 I agree with you. In my opinion this must be addressed in the type system. The importance of detected not awaited promises is that in the 99% of the cases that is an error (the omission was not intentional).

I also have the same opinion about nodiscard it is preferable to be in the type system. Types that can be non discartables. I don't know if a new keyword is needed for this. But sounds pretty if we can write:

function dividie(a:number, b:number):NonDiscard<{result:number, error:string}> ...

and that Promise<T> = NonDiscard<Promise<T>>.

Ditto that. I realize that this Result value must be used is more mature than nondiscard because the scope can be wider (What happens with NonDiscard<{result:number, error:NonDiscard<string>}>?). And is more necesary because it'll catch error that are very difficult to detect (because is some cases it only fails in certain raise conditions).

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 7, 2023

Breaks, part 1: #53146 (comment)
Breaks, part 2: #53146 (comment)

Even for something being considered as an opt-in, this is pretty bad as far as results go. Puppeteer in particular has dozens of errors, of which none of them appear to be legitimate.

Probably more work needs to be done here to figure out a rule that doesn't have a high false positive rate, or make the case that at least one of the hits in that PR are good finds. I think it's a hard sell that webpack has dozens of code smells.

On the plus side, perf was not meaningfully impacted.

@TimvdLippe
Copy link
Contributor

FWIW I am familiar with the Puppeteer codebase and it doesn't surprise me that it would find a lot of issues. Floating promises are a major painpoint for DevTools and Puppeteer which adopted Promises in the same era. Therefore, I wouldn't immediately write them all off as false positives. That said, it is quite a list and it probably needs extensive analysis which are legitimately floating and which aren't.

@jpike88
Copy link

jpike88 commented Mar 8, 2023

@RyanCavanaugh I personally think by skimming through the examples you provided, they are coding in much lower level styles in which case they shouldn't be opting in to something like this. I would also call looping over a lot of floating promises, or firing off a lot of floating promises in succession, a code smell. Promise.all exists for a reason!

I wouldn't consider anyway your findings to be a high false positive rate, quite a few of these files are for typing purposes only, and aren't worth trying to pore over due to their inapplicability to any practical case that wouldn't drive someone mad trying to figure out.

I think the best way to treat this is that it's OK and perfectly legitimate (rarely!) to be firing promises off without directly handling their results in the same 'thread', but TypeScript should give you a way to distinguish between a promise that you are floating either by accident or on purpose, which currently is impossible to tell as no visual markers are enforced to qualify the intention of the Promise usage.

So in other words, those library maintainers if they choose to opt-in can just place the void operator in front of any promise to pass the rule, and at the same time other developers will enjoy the benefit of understanding that the floating promises were written intentionally, with all the async complications that may follow. It would be easy to do and bring richer context to code in general, everybody wins.

@emilioplatzer
Copy link

emilioplatzer commented Mar 9, 2023

I expect that in the current state of my codebase all of the floating promises that you can find are legitims (thanks for our testers). But I'm sure that all of the floating promises that you can find in the diffs of all commits in my repository are buggy ones.

I was thinking about how to indicate that a promise is not floating. I can use void hoping that will be the solution. But what if is not? Maybe I can add a new function letItFloat to my code:

// @ts-expect-error: letting float a promise on purpose
function letItFloat(Promise<any> _) {}

And use it like:

async function runForever() {
   while (var x = await producer()) {
      await consumer(x)
   }
}

letItFloat(runForever());

Tomorrow I can replace letItFloat with void .

tsconfig

And maybe this feature can be turned on in tsconfig and being false by default.

@jpike88
Copy link

jpike88 commented Mar 27, 2023

We just ran into a problem again where a un-awaited promise was pushed to production (but managed to slip through automated and QA tests due to timing differences), which would have resulted in a very nasty, hard to diagnose bug. I only caught it because I decided to pore over the closed PRs a second time out of boredom. If discussion around this could intensify until a resolution is reached that would be ideal.

@dsherret
Copy link
Contributor

dsherret commented Jun 27, 2023

Probably more work needs to be done here to figure out a rule that doesn't have a high false positive rate, or make the case that at least one of the hits in that PR are good finds. I think it's a hard sell that webpack has dozens of code smells.

Detection of floating promises is probably my most wanted feature. I very often forget to await a promise and tracing down these kind of bugs without tooling can be difficult to spot. It's true there is a high false positive rate for this one, but I think it's good practice to explicitly opt-out of false positives to make it clear that was intended to the reader. For example, the C# compiler warns when not awaiting or assigning a task and I've always found that super helpful.

Ability for API authors to supress the error

Perhaps for APIs that are meant to be "fire and forget" and optionally awaited there could be a way for API authors to define that in order to supress this error for API consumers.

For example:

async function serve1(handler: ...): Promise<void> {
  // ...
}

async function serve2(handler: ...): Floatable<Promise<void>> {
  // ...
}

// error, unawaited promise (add `void` to supress)
serve1(() => { ... });

// no error, promise is floatable
serve2(() => { ... });

...where Floatable<T extends PromiseLike> is a built-in type (maybe for most cases people could just use type FloatablePromise<T> = Floatable<Promise<T>>).

Calling Async in Sync

Regarding calling an async function in a sync function: yes, I believe this should error similarly to how it should in a top level module statement. It's super rare to call an async function from a sync one. I very commonly do this by accident. I'd rather be verbose opting out of false positives with a void keyword in order to make my intent clear and catch bugs in scenarios where I didn't mean to do that.

@phaux
Copy link

phaux commented Dec 4, 2024

Perhaps for APIs that are meant to be "fire and forget" and optionally awaited there could be a way for API authors to define that in order to supress this error for API consumers.

...where Floatable<T extends PromiseLike> is a built-in type (maybe for most cases people could just use type FloatablePromise<T> = Floatable<Promise<T>>).

or add error type to Promise and make Promise<T, never> floatable by default.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests