TypeScript "using" keyword and console.group are a match made in heaven

Never forget to close a group again

TypeScript "using" keyword and console.group are a match made in heaven

Here is the TypeScript playground used for this example.

Let’s set up a (rather contrived) problem to motivate the use of the new using keyword introduced in TypeScript 5.2:

example.ts
const myFunction = () => {
console.group("myFunction")
const myNum = Math.random() * 100
if (myNum < 20) {
console.log("didn't work ")
return false
}
console.log("works!")
return true
}
myFunction()

We have a function here that fails sometimes. We want to debug it. We can use console.group to group the logs for this function together. But we need to remember to close the group at the end of the function. If we forget, the groups will continue ad-infinitum, every time the function is called.

Nested console.log groups

Of course, it’s not a big deal to just call console.groupEnd before every return:

example.ts
const myFunction = () => {
console.group("myFunction")
const myNum = Math.random() * 100
if (myNum < 20) {
console.log("didn't work ")
console.groupEnd()
return false
}
console.log("works!")
console.groupEnd()
return true
}
myFunction()

But suppose it is a hairy function with many returns. It’s easy to forget to close the group. We can use the using keyword to ensure that the group is closed when the function returns.

To start, we need to polyfill Symbol.dispose, which is still in TC39 stage 3.

example.ts
// <polyfill>
// the interfaces are only necessary if you're not including https://github.com/microsoft/TypeScript/blob/main/src/lib/esnext.disposable.d.ts as a `lib` option
interface SymbolConstructor {
readonly dispose: unique symbol
}
interface Disposable {
[Symbol.dispose](): void
}
// @ts-ignore - if it already exists as a readonly property, this is a no-op anyway
Symbol.dispose ??= Symbol('Symbol.dispose')
// </polyfill>
const myFunction = () => {
console.group("myFunction")
const myNum = Math.random() * 100
if (myNum < 20) {
console.log("didn't work ")
console.groupEnd()
return false
}
console.log("works!")
console.groupEnd()
return true
}
myFunction()

Then, we need to define a disposable resource:

example.ts
15 collapsed lines
// <polyfill>
// the interfaces are only necessary if you're not including https://github.com/microsoft/TypeScript/blob/main/src/lib/esnext.disposable.d.ts as a `lib` option
interface SymbolConstructor {
readonly dispose: unique symbol
}
interface Disposable {
[Symbol.dispose](): void
}
// @ts-ignore - if it already exists as a readonly property, this is a no-op anyway
Symbol.dispose ??= Symbol('Symbol.dispose')
// </polyfill>
const logGroup = (groupName: string) => {
console.group(groupName)
return {
[Symbol.dispose]: () => {
console.groupEnd()
}
}
}
const myFunction = () => {
const myNum = Math.random() * 100
if (myNum < 20) {
console.log("didn't work ")
return false
}
console.log("works!")
return true
}
myFunction()

When the return value of logGroup leaves scope, it will be disposed. We can use the using keyword for this:

example.ts
15 collapsed lines
// <polyfill>
// the interfaces are only necessary if you're not including https://github.com/microsoft/TypeScript/blob/main/src/lib/esnext.disposable.d.ts as a `lib` option
interface SymbolConstructor {
readonly dispose: unique symbol
}
interface Disposable {
[Symbol.dispose](): void
}
// @ts-ignore - if it already exists as a readonly property, this is a no-op anyway
Symbol.dispose ??= Symbol('Symbol.dispose')
// </polyfill>
const logGroup = (groupName: string) => {
console.group(groupName)
return {
[Symbol.dispose]: () => {
console.groupEnd()
}
}
}
const myFunction = () => {
using _ = logGroup("myFunction")
const myNum = Math.random() * 100
if (myNum < 20) {
console.log("didn't work ")
return false
}
console.log("works!")
return true
}
myFunction()

Now our groups are always closed:

flat console.log groups

Learn more about what you can do with using here: https://www.totaltypescript.com/typescript-5-2-new-keyword-using.