[ts][lint] Improve typecheck and linting #3

Include the `tseslint.configs.recommendedTypeChecked` preset
configuration and refactor code to fix the new lint warnings.

Bug: 497015838
Change-Id: I6deb465b6ec5a2962ad4d229865c3dc692924d90
Reviewed-on: https://fuchsia-review.googlesource.com/c/vscode-plugins/+/1559520
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Jacob Rutherford <jruthe@google.com>
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 9c136c1..bdbcd6e 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -11,6 +11,12 @@
 export default defineConfig(
   eslint.configs.recommended,
   tseslint.configs.recommended,
+  ...tseslint.configs.recommendedTypeChecked.map(config => ({
+    ...config,
+
+    // Only apply type-checked rules to TypeScript files.
+    files: ['**/*.ts'],
+  })),
   [
     {
       ignores: ['.vscode-test/**', 'dist/**', 'test-dist/**'],
@@ -47,27 +53,19 @@
     // TypeScript specific rules
     {
       files: ['**/*.ts'],
-      plugins: {
-        '@typescript-eslint': tseslint.plugin,
-      },
-
       languageOptions: {
-        parser: tseslint.parser,
-        ecmaVersion: 'latest',
-        sourceType: 'module',
-
         parserOptions: {
           project: 'tsconfig.json',
         },
       },
 
+      plugins: {
+        '@typescript-eslint': tseslint.plugin,
+      },
+
       rules: {
-        // Overrides/additions to recommended
+        // Additions to recommended
         '@typescript-eslint/naming-convention': 'error',
-        '@typescript-eslint/no-floating-promises': 'error',
-        '@typescript-eslint/no-misused-promises': 'error',
-        '@typescript-eslint/only-throw-error': 'error',
-        '@typescript-eslint/require-await': 'error',
       },
     },
 
diff --git a/src/analytics/ga4.ts b/src/analytics/ga4.ts
index 5f3f5fd..a64e78e 100644
--- a/src/analytics/ga4.ts
+++ b/src/analytics/ga4.ts
@@ -70,21 +70,29 @@
     try {
       const req = https.request(options, (resp) => {
         if (debugLevel > 0) {
+          let responseText = '';
+          resp.setEncoding('utf-8');
           resp.on('data', (c: Buffer | string) => {
+            responseText += c.toString();
+          });
+          resp.on('end', () => {
             try {
-              const gaDebugResp = JSON.parse(c.toString());
-              if (gaDebugResp && gaDebugResp.validationMessages &&
-                gaDebugResp.validationMessages.length === 0) {
-                logger.debug('GA4 Sent OK!');
-              } else if (
-                gaDebugResp && gaDebugResp.validationMessages &&
-                gaDebugResp.validationMessages.length > 0) {
-                logger.debug(`Invalid GA4 hit: ${c?.toString()}`);
-              } else {
-                logger.debug(`Unexpected GA4 debug response: ${c?.toString()}`);
+              const gaDebugResp = JSON.parse(responseText) as unknown;
+              if (typeof gaDebugResp !== 'object' ||
+                gaDebugResp === null ||
+                !('validationMessages' in gaDebugResp) ||
+                !Array.isArray(gaDebugResp.validationMessages)) {
+                logger.debug(`Unexpected GA4 debug response: ${responseText}`);
+                return;
               }
+              if (gaDebugResp.validationMessages.length > 0) {
+                logger.debug(`Invalid GA4 hit: ${responseText}`);
+                return;
+              }
+
+              logger.debug('GA4 Sent OK!');
             } catch {
-              logger.error(`Error in GA4 debug response: ${c?.toString()}`);
+              logger.error(`Error in GA4 debug response: ${responseText}`);
             }
           });
         }
@@ -113,7 +121,7 @@
  * @param e the captured error
  */
 function handleError(e: unknown) {
-  console.log(`Failed to send analytics, disabling for session: ${e}`);
+  console.log(`Failed to send analytics, disabling for session: ${String(e)}`);
 }
 
 /**
diff --git a/src/analytics/info.ts b/src/analytics/info.ts
index 0c6ef87..9a7105e 100644
--- a/src/analytics/info.ts
+++ b/src/analytics/info.ts
@@ -57,12 +57,12 @@
     return AnalyticsInfo.instance.isInternalUserInternal;
   }
 
-  static async init(context: vscode.ExtensionContext): Promise<void> {
-    await AnalyticsInfo.instance.initInternal(context);
+  static init(context: vscode.ExtensionContext): void {
+    AnalyticsInfo.instance.initInternal(context);
   }
 
   private initInternal(context: vscode.ExtensionContext): void {
-    const packageJson = context.extension.packageJSON;
+    const packageJson = context.extension.packageJSON as { version: string };
     this.extensionVersionInternal = packageJson.version;
     this.osInternal = os.type();
     this.archInternal = os.machine();
diff --git a/src/analytics/init.ts b/src/analytics/init.ts
index 044f26b..c022607 100644
--- a/src/analytics/init.ts
+++ b/src/analytics/init.ts
@@ -20,7 +20,7 @@
     return;
   }
 
-  await AnalyticsInfo.init(context);
+  AnalyticsInfo.init(context);
   AnalyticsState.initDebugLevel();
 
   if (AnalyticsInfo.isInternalUser) {
diff --git a/src/components.ts b/src/components.ts
index da728a5..9f90285 100644
--- a/src/components.ts
+++ b/src/components.ts
@@ -103,8 +103,8 @@
     registerCommandWithAnalyticsEvent(
       command, async (info?: ComponentInfo) => {
         const args = [...commandOptions.args];
-        let moniker: string | undefined = info?.moniker.value ?? '';
-        let url: string | undefined = info?.url ?? '';
+        let moniker: string | undefined = info?.moniker.value;
+        let url: string | undefined = info?.url;
 
         if (!moniker) {
           moniker = await vscode.window.showInputBox({
@@ -116,22 +116,22 @@
         if (!moniker) {
           return;
         }
-        if (!url && commandOptions.requiresURL) {
-          url = await vscode.window.showInputBox({
-            placeHolder: '(e.g. fuchsia-pkg://fuchsia.com/hello-world-cpp#meta/hello-world-cpp.cm)',
-            value: '',
-            prompt: 'Enter a component URL'
-          });
-        }
-        if (!url && commandOptions.requiresURL) {
-          return;
-        }
+        args.push(moniker);
 
         if (commandOptions.requiresURL) {
-          args.push(moniker!, url!);
-        } else {
-          args.push(moniker!);
+          if (!url) {
+            url = await vscode.window.showInputBox({
+              placeHolder: '(e.g. fuchsia-pkg://fuchsia.com/hello-world-cpp#meta/hello-world-cpp.cm)',
+              value: '',
+              prompt: 'Enter a component URL'
+            });
+          }
+          if (!url) {
+            return;
+          }
+          args.push(url);
         }
+
         await setup.ffx.runFfx(args);
 
         if (commandOptions.refresh) {
@@ -259,7 +259,7 @@
     return new ComponentTreeItem(element, collapsibleState);
   }
 
-  async getChildren(element?: ComponentInfo | undefined): Promise<ComponentInfo[]> {
+  async getChildren(element?: ComponentInfo): Promise<ComponentInfo[]> {
     if (element) {
       return element.children;
     } else if (this.currentDevice) {
@@ -400,7 +400,7 @@
     return new TaskTreeItem(element, collapsibleState);
   }
 
-  async getChildren(element?: TaskInfo | undefined): Promise<TaskInfo[]> {
+  async getChildren(element?: TaskInfo): Promise<TaskInfo[]> {
     if (element) {
       return element.children;
     } else if (this.currentDevice) {
diff --git a/src/doc_provider.ts b/src/doc_provider.ts
index b4b9ba7..c18fe82 100644
--- a/src/doc_provider.ts
+++ b/src/doc_provider.ts
@@ -52,8 +52,8 @@
       const fileUri = vscode.Uri.file(fullPath);
 
       // The range in the document to underline link
-      const start = document.positionAt(fileMatch.index!);
-      const end = document.positionAt(fileMatch.index! + linkText.length);
+      const start = document.positionAt(fileMatch.index);
+      const end = document.positionAt(fileMatch.index + linkText.length);
       const linkRange = new vscode.Range(start, end);
 
       // Link format is file:///path/to/file.ts#Lline,column
diff --git a/src/extension.ts b/src/extension.ts
index a7aced1..593eb29 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -153,7 +153,7 @@
               const output = await command();
               logger.info(output, 'fuchsia');
             } catch (err: unknown) {
-              logger.error(`${err}`);
+              logger.error(`${String(err)}`);
               void vscode.window.showErrorMessage(
                 `error with ${commandName}, see output for details`);
             }
@@ -190,7 +190,7 @@
             const output = await command(device);
             logger.info(output, commandName);
           } catch (err: unknown) {
-            logger.error(`${err}`, commandName);
+            logger.error(`${String(err)}`, commandName);
             void vscode.window.showErrorMessage(
               `error with ${commandName}, see output for details`);
           }
diff --git a/src/ffx.ts b/src/ffx.ts
index 568d7bd..b5337f0 100644
--- a/src/ffx.ts
+++ b/src/ffx.ts
@@ -24,7 +24,7 @@
   public readonly isFfxDefault: boolean = false;
 
   constructor(data: Record<string, unknown>) {
-    this._data = JSON.parse(JSON.stringify(data));
+    this._data = JSON.parse(JSON.stringify(data)) as Record<string, unknown>;
     this.nodeName = (data['nodename'] as string) ?? '<not set>!';
     this.connected = data['rcs_state'] === 'Y';
     this.serial = (data['serial'] as string) ?? '';
@@ -320,7 +320,7 @@
 
     if (this.path) {
       timeoutMillis = timeoutMillis ?? vscode.workspace.getConfiguration().get('fuchsia.connectionTimeout');
-      const devices = (await this.runFfxJson(['target', 'list'], timeoutMillis, null)) as Record<string, unknown>[];
+      const devices = await this.runFfxJson<Record<string, unknown>[]>(['target', 'list'], timeoutMillis, null);
 
       for (const obj of devices) {
         const device = new FuchsiaDevice(obj);
@@ -419,7 +419,7 @@
     // Default `device` to the default device's nodeName, if available.
     if (device === undefined) {
       if (!this.targetDevice) {
-        throw new Error(`Unable to resolve the default target for ffx command: ${args}`);
+        throw new Error(`Unable to resolve the default target for ffx command: ${args.join(' ')}`);
       }
       device = this.targetDevice.nodeName;
     }
@@ -521,7 +521,7 @@
       try {
         cmd = this.runFfxStreaming(args, device);
       } catch (e) {
-        return reject(e);
+        return reject(e instanceof Error ? e : new Error(String(e)));
       }
       let hasTimedOut = false;
       let timeout: NodeJS.Timeout | undefined = undefined;
@@ -556,7 +556,7 @@
       cmd?.on('close', (code, signal) => {
         if (!hasError) {
           if (hasTimedOut) {
-            logger.debug(`Timeout: args = ${args}`, 'ffx');
+            logger.debug(`Timeout: args = ${args.join(' ')}`, 'ffx');
             return reject(new Error('ffx command timeout'));
           } else if (timeout) {
             clearTimeout(timeout);
diff --git a/src/fx.ts b/src/fx.ts
index dab87de..10c96e6 100644
--- a/src/fx.ts
+++ b/src/fx.ts
@@ -98,7 +98,7 @@
       this.runStreaming(args),
       onData,
       (data) => {
-        logger.warn(`Error [fx ${args}]: ${data}`);
+        logger.warn(`Error [fx ${args.join(' ')}]: ${data.toString()}`);
       });
   }
 
diff --git a/src/git_helper.ts b/src/git_helper.ts
index 02a0b68..79dd7f4 100644
--- a/src/git_helper.ts
+++ b/src/git_helper.ts
@@ -17,8 +17,8 @@
  * Initialize the openInCodeSearch command.
  */
 export function setUpGitHelper(ctx: vscode.ExtensionContext) {
-  ctx.subscriptions.push(vscode.commands.registerCommand('fuchsia.openInCodeSearch', async (arg) => {
-    await openAtMain(arg);
+  ctx.subscriptions.push(vscode.commands.registerCommand('fuchsia.openInCodeSearch', async (arg: unknown) => {
+    await openAtMain(arg as ContextMenuCommandArg | undefined);
   }));
 }
 
@@ -41,7 +41,7 @@
 /**
  * Returns the path segment within the Fuchsia tree for the given context menu argument.
  */
-function getPathSegment(arg: ContextMenuCommandArg) {
+function getPathSegment(arg?: ContextMenuCommandArg) {
   const root = getRoot()?.fsPath;
 
   const fullPath =
@@ -80,7 +80,7 @@
 /**
  * Opens the current file, at the current line, on branch main in osscs.
  */
-async function openAtMain(arg: ContextMenuCommandArg) {
+async function openAtMain(arg?: ContextMenuCommandArg) {
   const cutPath = getPathSegment(arg);
   const line = getLineSegment();
   await vscode.commands.executeCommand('vscode.open', makeUrl(cutPath, line));
diff --git a/src/process.ts b/src/process.ts
index f12d139..2adad98 100644
--- a/src/process.ts
+++ b/src/process.ts
@@ -5,9 +5,6 @@
 import { type ChildProcessWithoutNullStreams } from 'child_process';
 import JsonParser from 'jsonparse';
 
-/**
- * A stream of Data coming from a process.
- */
 export class DataStreamProcess {
   private outputChunks: Buffer[] = [];
 
@@ -16,13 +13,19 @@
     onData: (data: Buffer) => void,
     onError: (data: Buffer) => void,
   ) {
-    this.process.stdout.on('data', chunk => {
-      this.outputChunks.push(chunk);
-      onData(chunk);
+    this.process.stdout.on('data', (chunk: unknown) => {
+      const buffer = Buffer.isBuffer(chunk) ?
+        chunk :
+        Buffer.from(typeof chunk === 'string' ? chunk : String(chunk));
+      this.outputChunks.push(buffer);
+      onData(buffer);
     });
-    this.process.stderr.on('data', chunk => {
-      this.outputChunks.push(chunk);
-      onError(chunk);
+    this.process.stderr.on('data', (chunk: unknown) => {
+      const buffer = Buffer.isBuffer(chunk) ?
+        chunk :
+        Buffer.from(typeof chunk === 'string' ? chunk : String(chunk));
+      this.outputChunks.push(buffer);
+      onError(buffer);
     });
   }
 
diff --git a/src/test/suite/commands.test.ts b/src/test/suite/commands.test.ts
index 4fefd91..667390c 100644
--- a/src/test/suite/commands.test.ts
+++ b/src/test/suite/commands.test.ts
@@ -14,7 +14,11 @@
   let registeredCommands: string[];
   before(async function() {
     const packageDir = path.join(__dirname, '..');
-    const packageJSON = JSON.parse(await fs.readFile(path.join(packageDir, 'package.json'), 'utf8'));
+    const packageJSON = JSON.parse(await fs.readFile(path.join(packageDir, 'package.json'), 'utf8')) as {
+      contributes: {
+        commands: { command: string }[];
+      };
+    };
 
     // sort to make comparison easier
     userCommands = packageJSON.contributes.commands.
diff --git a/src/test/suite/ffx.test.ts b/src/test/suite/ffx.test.ts
index ebe6668..35a096d 100644
--- a/src/test/suite/ffx.test.ts
+++ b/src/test/suite/ffx.test.ts
@@ -35,8 +35,8 @@
         assert.strictEqual(device.targetState, data['target_state']);
         const testAddresses = data['addresses'];
         const actualAddresses = device.addresses;
-        for (const i in testAddresses) {
-          assert.strictEqual(testAddresses[i], actualAddresses[i]);
+        for (const [i, address] of testAddresses.entries()) {
+          assert.strictEqual(address, actualAddresses[i]);
         }
       });
   });
@@ -123,7 +123,7 @@
 
       const ffx = new Ffx(TEST_CWD, TEST_FFX);
 
-      const errorMessage = 'Unable to resolve the default target for ffx command: target,reboot';
+      const errorMessage = 'Unable to resolve the default target for ffx command: target reboot';
       await assert.rejects(() => ffx.rebootTarget(), new Error(errorMessage));
       assert.deepStrictEqual(actualInvocations, expectedInvocations);
     });
@@ -469,7 +469,7 @@
     it('does not call ffx target off without a target', async function() {
       const ffx = new Ffx(TEST_CWD, TEST_FFX);
 
-      const errorMessage = 'Unable to resolve the default target for ffx command: target,off';
+      const errorMessage = 'Unable to resolve the default target for ffx command: target off';
       const {actualInvocations, expectedInvocations} = mockFfx();
       await assert.rejects(() => ffx.poweroffTarget(), new Error(errorMessage));
       assert.deepStrictEqual(actualInvocations, expectedInvocations);
@@ -654,7 +654,7 @@
       const {actualInvocations, expectedInvocations} = mockFfx();
       const ffx = new Ffx(TEST_CWD, TEST_FFX);
 
-      const errorMessage = 'Unable to resolve the default target for ffx command: target,show';
+      const errorMessage = 'Unable to resolve the default target for ffx command: target show';
       await assert.rejects(() => ffx.showTarget(), new Error(errorMessage));
       assert.deepStrictEqual(actualInvocations, expectedInvocations);
     });
@@ -716,7 +716,7 @@
       const {actualInvocations, expectedInvocations} = mockFfx();
       const ffx = new Ffx(TEST_CWD, TEST_FFX);
 
-      const errorMessage = 'Unable to resolve the default target for ffx command: target,snapshot,-d,/path/to/workspace';
+      const errorMessage = 'Unable to resolve the default target for ffx command: target snapshot -d /path/to/workspace';
       await assert.rejects(() => ffx.exportSnapshotToCWD(), new Error(errorMessage));
       assert.deepStrictEqual(actualInvocations, expectedInvocations);
     });
diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts
index b3a135f..37348f4 100644
--- a/src/test/suite/index.ts
+++ b/src/test/suite/index.ts
@@ -40,7 +40,7 @@
       });
     } catch (err) {
       console.error(err);
-      e(err);
+      e(err instanceof Error ? err : new Error(String(err)));
     }
   });
 }
diff --git a/src/test/suite/problem_matcher.test.ts b/src/test/suite/problem_matcher.test.ts
index e9017c3..90ca7ee 100644
--- a/src/test/suite/problem_matcher.test.ts
+++ b/src/test/suite/problem_matcher.test.ts
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import { FuchsiaDiagnostics } from '../../workflow/problem_matcher';
+import { FuchsiaDiagnostics, type PackageJsonStub } from '../../workflow/problem_matcher';
 import { describe, it } from 'mocha';
 import assert from 'assert';
 import * as vscode from 'vscode';
@@ -20,7 +20,7 @@
   ];
 
   const ext = vscode.extensions.getExtension('fuchsia-authors.vscode-fuchsia');
-  const diagnostics = new FuchsiaDiagnostics(ext?.packageJSON);
+  const diagnostics = new FuchsiaDiagnostics(ext?.packageJSON as PackageJsonStub);
 
   this.beforeEach(function () {
     diagnostics.clear();
diff --git a/src/test/suite/reporter.ts b/src/test/suite/reporter.ts
index 0f7943f..ff200b1 100644
--- a/src/test/suite/reporter.ts
+++ b/src/test/suite/reporter.ts
@@ -4,6 +4,7 @@
 
 import mocha from 'mocha';
 import * as fs from 'fs';
+import * as stream from 'stream';
 
 
 type SuiteChild = Suite | Test;
@@ -34,18 +35,21 @@
 }
 
 export default class TestReporter extends mocha.reporters.Base {
-  out: fs.WriteStream;
+  out: stream.Writable;
 
   constructor(runner: mocha.Runner, options: mocha.MochaOptions) {
     super(runner, options);
 
-    const outputName = options?.reporterOptions?.output;
-    this.out = fs.createWriteStream(outputName);
+    const reporterOptions = options?.reporterOptions as Record<string, unknown> | undefined;
+    if (typeof reporterOptions?.output === 'string') {
+      this.out = fs.createWriteStream(reporterOptions.output);
+    } else {
+      this.out = process.stdout;
+    }
 
-    const suiteName = options?.reporterOptions?.suiteName ?? 'Mocha Tests';
+    const suiteName = (reporterOptions?.suiteName as string) ?? 'Mocha Tests';
 
-    const suiteStack: Suite[] = [
-    ];
+    const suiteStack: Suite[] = [];
     let currentSuite: Suite = { // root
       name: suiteName,
       children: [],
@@ -120,9 +124,9 @@
     this.out.write(`<testsuite name="${suite.name.replaceAll('"', '&quot;')}" failures="${suite.failures}" tests="${suite.tests}">\n`);
     for (const child of suite.children) {
       if ('status' in child) {
-        this.writeTest(child as Test);
+        this.writeTest(child);
       } else {
-        this.writeSuite(child as Suite);
+        this.writeSuite(child);
       }
     }
     this.out.write('</testsuite>\n');
diff --git a/src/test/suite/snap.test.ts b/src/test/suite/snap.test.ts
index 5140524..3f83793 100644
--- a/src/test/suite/snap.test.ts
+++ b/src/test/suite/snap.test.ts
@@ -49,7 +49,7 @@
       }
 
       // need the filename to indicate that this is JSON, no a plist
-      return await tm.parseRawGrammar(contents, `${ext}.json`);
+      return tm.parseRawGrammar(contents, `${ext}.json`);
     }
   });
 }
diff --git a/src/test/suite/target_status_bar_item.test.ts b/src/test/suite/target_status_bar_item.test.ts
index c979360..7b2870c 100644
--- a/src/test/suite/target_status_bar_item.test.ts
+++ b/src/test/suite/target_status_bar_item.test.ts
@@ -164,7 +164,7 @@
           // Check that the item selected exists.
           if (!itemSelection) {
             quickPickErrors.push(new Error(
-              `Selection = "${selection}" is not in items = [${items.map(e => e.label)}]`
+              `Selection = "${selection ?? 'undefined'}" is not in items = [${items.map(e => e.label).join(', ')}]`
             ));
           }
 
diff --git a/src/test/suite/test_controller/discovery.test.ts b/src/test/suite/test_controller/discovery.test.ts
index 6ca748d..9b6981c 100644
--- a/src/test/suite/test_controller/discovery.test.ts
+++ b/src/test/suite/test_controller/discovery.test.ts
@@ -358,12 +358,13 @@
         const barTestItem = controller.createTestItem('bar', 'bar');
 
 
-        const discoverFn = sandbox.stub().callsFake((_controller, _fx, testItem) => {
-          testItem.children.add(
-            controller.createTestItem(`${testItem.id}.testCase`, `${testItem.id}.testCase`)
-          );
-          return Promise.resolve();
-        });
+        const discoverFn = sandbox.stub().callsFake(
+          (_controller, _fx, testItem: vscode.TestItem) => {
+            testItem.children.add(
+              controller.createTestItem(`${testItem.id}.testCase`, `${testItem.id}.testCase`)
+            );
+            return Promise.resolve();
+          });
 
         const queue = TestcaseDiscoveryQueue.createForTesting(controller, fx, discoverFn);
 
@@ -383,12 +384,13 @@
         const fooTestItem = controller.createTestItem('foo', 'foo');
         const barTestItem = controller.createTestItem('bar', 'bar');
 
-        const discoverFn = sandbox.stub().callsFake(async (_controller, _fx, testItem) => {
-          await new Promise(resolve => setTimeout(resolve, 50));
-          testItem.children.add(
-            controller.createTestItem(`${testItem.id}.testCase`, `${testItem.id}.testCase`)
-          );
-        });
+        const discoverFn = sandbox.stub().callsFake(
+          async (_controller, _fx, testItem: vscode.TestItem) => {
+            await new Promise(resolve => setTimeout(resolve, 50));
+            testItem.children.add(
+              controller.createTestItem(`${testItem.id}.testCase`, `${testItem.id}.testCase`)
+            );
+          });
 
         const queue = TestcaseDiscoveryQueue.createForTesting(controller, fx, discoverFn);
 
diff --git a/src/test/suite/utils.ts b/src/test/suite/utils.ts
index ea66051..e355a9b 100644
--- a/src/test/suite/utils.ts
+++ b/src/test/suite/utils.ts
@@ -10,7 +10,7 @@
 import * as vscode from 'vscode';
 
 // eslint-disable-next-line @typescript-eslint/no-require-imports
-const childProcess = require('child_process');
+const childProcess = require('child_process') as typeof import('child_process');
 
 /**
  * Waits for a single macrotask to complete on the macrotask event loop.
@@ -87,9 +87,10 @@
  */
 export function createFakeChildProcess(): ChildProcess {
   const spawnEvent: ChildProcess = new EventEmitter() as ChildProcess;
-  spawnEvent.stdout = new EventEmitter() as Readable;
-  spawnEvent.stderr = new EventEmitter() as Readable;
-  spawnEvent.kill = (signal?: number | undefined) => {
+  spawnEvent.stdout = new Readable({ read() {} });
+  spawnEvent.stderr = new Readable({ read() {} });
+
+  spawnEvent.kill = (signal?: number) => {
     spawnEvent.emit('exit', signal);
     spawnEvent.emit('close', signal);
     return true;
@@ -255,7 +256,8 @@
     setTimeout(() => {
       if (command !== expectedInvocations[idx]?.at(0)) {
         console.warn(
-          `[setupMockedCommand] Ignoring non-matching command ${command}, ${args}, ${options}`
+          `[setupMockedCommand] Ignoring non-matching command ${command}, ` +
+          `${JSON.stringify(args)}, ${JSON.stringify(options)}`
         );
         fakeSpawnedProcess.emit('exit', 0);
         fakeSpawnedProcess.emit('close', 0);
@@ -271,7 +273,7 @@
       if (JSON.stringify(expectedArgs) !== JSON.stringify(args) || options.cwd !== cwd) {
         const errmsg = `[setupMockedCommand] Unexpected call.
 Expected invocations:\n${expectedInvocations.join('\n')}, CWD=${cwd}
-Actual invocations:\n${actualInvocations.join('\n')}, CWD=${options.cwd}`;
+Actual invocations:\n${actualInvocations.join('\n')}, CWD=${String(options.cwd)}`;
         console.error(errmsg);
         fakeSpawnedProcess.stderr?.emit('data', JSON.stringify(errmsg));
         fakeSpawnedProcess.emit('exit', 1);
diff --git a/src/test/suite/workflow.test.ts b/src/test/suite/workflow.test.ts
index ecb99d4..166d7d6 100644
--- a/src/test/suite/workflow.test.ts
+++ b/src/test/suite/workflow.test.ts
@@ -137,7 +137,7 @@
       const selection = pkgHistory.update([] as Array<Packages>);
       const packageList = pkgHistory.toList(selection);
       packageList.forEach((pkg) => {
-        if (pkg.kind !== -1) {
+        if (pkg.kind !== vscode.QuickPickItemKind.Separator) {
           assert.equal(pkg.picked, false);
         }
       });
diff --git a/src/test/suite/zxdb/async_backtrace.test.ts b/src/test/suite/zxdb/async_backtrace.test.ts
index 51ead2e..6e856cc 100644
--- a/src/test/suite/zxdb/async_backtrace.test.ts
+++ b/src/test/suite/zxdb/async_backtrace.test.ts
@@ -336,13 +336,13 @@
     it('should jump to definition with an absolute path', function () {
       const item = provider.getTreeItem(taskA1);
       assert.ok(item.command);
-      const cmd = item.command!;
+      const cmd = item.command;
       assert.strictEqual(cmd.command, 'vscode.open');
       assert.ok(cmd.arguments);
-      assert.strictEqual(cmd.arguments!.length, 2);
+      assert.strictEqual(cmd.arguments.length, 2);
       const expectedPath = vscode.Uri.file('/path/to/a1.ts').fsPath;
-      assert.strictEqual((cmd.arguments![0] as vscode.Uri).fsPath, expectedPath);
-      assert.deepStrictEqual(cmd.arguments![1], {
+      assert.strictEqual((cmd.arguments[0] as vscode.Uri).fsPath, expectedPath);
+      assert.deepStrictEqual(cmd.arguments[1], {
         selection: new vscode.Range(9, 0, 9, 0)
       });
     });
@@ -350,13 +350,13 @@
     it('should jump to definition with a relative path', function () {
       const item = provider.getTreeItem(taskLong);
       assert.ok(item.command);
-      const cmd = item.command!;
+      const cmd = item.command;
       assert.strictEqual(cmd.command, 'vscode.open');
       assert.ok(cmd.arguments);
-      assert.strictEqual(cmd.arguments!.length, 2);
+      assert.strictEqual(cmd.arguments.length, 2);
       const expectedPath = vscode.Uri.file('/some/workspace/relative/path/to/long.ts').fsPath;
-      assert.strictEqual((cmd.arguments![0] as vscode.Uri).fsPath, expectedPath);
-      assert.deepStrictEqual(cmd.arguments![1], {
+      assert.strictEqual((cmd.arguments[0] as vscode.Uri).fsPath, expectedPath);
+      assert.deepStrictEqual(cmd.arguments[1], {
         selection: new vscode.Range(11, 0, 11, 0)
       });
     });
@@ -416,8 +416,8 @@
       const children = provider.getChildren() as AsyncBacktrace[];
       const updated = children.find(t => t.sessionId === 'session-1' && t.koid === 1);
       assert.ok(updated);
-      assert.strictEqual(updated!.name, 'foo1-updated');
-      assert.deepStrictEqual(updated!.children, [taskB1]);
+      assert.strictEqual(updated.name, 'foo1-updated');
+      assert.deepStrictEqual(updated.children, [taskB1]);
     });
 
     it('should add new thread and fire event', function () {
@@ -456,7 +456,7 @@
       const threads = provider.getChildren() as AsyncBacktrace[];
       const updated = threads.find(t => t.sessionId === 'session-1' && t.koid === 1);
       assert.ok(updated);
-      assert.deepStrictEqual(updated!.children, []);
+      assert.deepStrictEqual(updated.children, []);
 
       const tasks = provider.getChildren(emptyThread) as unknown[];
       assert.deepStrictEqual(tasks, []);
@@ -476,8 +476,8 @@
 
       assert.ok(foo1);
       assert.ok(foo2);
-      assert.strictEqual(foo1!.name, 'foo1-updated');
-      assert.strictEqual(foo2!.name, 'foo2'); // Verify foo2 was not overwritten
+      assert.strictEqual(foo1.name, 'foo1-updated');
+      assert.strictEqual(foo2.name, 'foo2'); // Verify foo2 was not overwritten
     });
   });
 
diff --git a/src/test/suite/zxdb/console.test.ts b/src/test/suite/zxdb/console.test.ts
index e148e57..5df2521 100644
--- a/src/test/suite/zxdb/console.test.ts
+++ b/src/test/suite/zxdb/console.test.ts
@@ -101,7 +101,7 @@
 
     // bypass "TypeError: Descriptor for property Socket is non-configurable and non-writable"
     // eslint-disable-next-line @typescript-eslint/no-require-imports
-    const mutableNet = require('net');
+    const mutableNet = require('net') as typeof Net;
     sandbox.stub(mutableNet, 'Socket').returns(socket);
 
     ffxCallArgs = [];
@@ -135,7 +135,7 @@
       listeningListener();
       return server;
     });
-    server.close.callsFake((callback?: ((err?: Error | undefined) => void) | undefined) => {
+    server.close.callsFake((callback?: ((err?: Error) => void)) => {
       if (callback) { callback(); }
       return server;
     });
diff --git a/src/test_controller/discovery.ts b/src/test_controller/discovery.ts
index 561434b..e01532e 100644
--- a/src/test_controller/discovery.ts
+++ b/src/test_controller/discovery.ts
@@ -143,7 +143,7 @@
           // or starting testcase discovery for a different test.
           this.queue.push([testItem, discoveredCallback]);
           logger.error(`Testcase discovery failed: ${testItem.id}`, 'discoverFromQueue');
-          logger.trace(`Error details: ${e}`, 'discoverFromQueue');
+          logger.trace(`Error details: ${String(e)}`, 'discoverFromQueue');
         }
 
         // After all pending discoveries settle, check if any were nonsuccessful and present an
@@ -197,9 +197,16 @@
 
         const bytes = await vscode.workspace.fs.readFile(uri);
         const content = new TextDecoder().decode(bytes);
-        const tests = JSON.parse(content);
+        const tests = JSON.parse(content) as unknown;
+        if (!Array.isArray(tests)) {
+          logger.error('tests.json does not contain a valid array');
+          return;
+        }
         for (const testDescriptor of tests) {
-          const test = testDescriptor.test;
+          if (typeof testDescriptor !== 'object' || testDescriptor === null || !('test' in testDescriptor)) {
+            continue;
+          }
+          const test = (testDescriptor as { test: unknown }).test;
           if (!test) {
             continue;
           }
@@ -243,7 +250,7 @@
           await scanTestsJson(testsJsonFile);
         } catch (ex) {
           // The tests.json file doesn't exist yet.
-          logger.debug(`Unable to scan tests.json: ${ex}`);
+          logger.debug(`Unable to scan tests.json: ${String(ex)}`);
         }
       }
 
@@ -261,7 +268,7 @@
         await scanBuildDir(fxBuildDirFile);
       } catch (ex) {
         // The .fx-build-dir file doesn't exist yet.
-        logger.debug(`Unable to scan .fx-build-dir: ${ex}`);
+        logger.debug(`Unable to scan .fx-build-dir: ${String(ex)}`);
       }
     })
   );
diff --git a/src/tool_finder.ts b/src/tool_finder.ts
index a37ed01..c14afc2 100644
--- a/src/tool_finder.ts
+++ b/src/tool_finder.ts
@@ -84,7 +84,7 @@
       if (!process.env.HOME) {
         throw new Error(`Unable to locate $HOME to expand ~ in tool path. path = "${toolPath}".`);
       }
-      return path.join(process.env.HOME, toolPath.substring(1)) as string;
+      return path.join(process.env.HOME, toolPath.substring(1));
     } else if (path.isAbsolute(toolPath)) {
       return toolPath;
     } else {
@@ -92,7 +92,7 @@
         throw new Error('Could not evaluate the workspace' +
           `relative path for ffx. path = "${toolPath}".`);
       }
-      return path.join(workspacePath, toolPath) as string;
+      return path.join(workspacePath, toolPath);
     }
   }
 
diff --git a/src/workflow/build.ts b/src/workflow/build.ts
index 7aa9ea9..421a1c5 100644
--- a/src/workflow/build.ts
+++ b/src/workflow/build.ts
@@ -127,7 +127,7 @@
           void window.showErrorMessage(
             'Build stopped, [see output for details](command:fuchsia.showOutput)'
           );
-          return Promise.reject(exitCode);
+          return Promise.reject(new Error(`fx build failed with exit code ${exitCode}`));
         }
       }
     );
@@ -161,7 +161,9 @@
         progress.report({ message: '[(details)](command:fuchsia.showOutput)' });
 
         const exitCode = await cmd?.exitCode;
-        return exitCode === 0 ? Promise.resolve() : Promise.reject(exitCode);
+        return exitCode === 0
+          ? Promise.resolve()
+          : Promise.reject(new Error(`fx ota failed with exit code ${exitCode}`));
       }
     );
   });
@@ -194,7 +196,9 @@
         progress.report({ message: '[(details)](command:fuchsia.showOutput)' });
 
         const exitCode = await cmd?.exitCode;
-        return exitCode === 0 ? Promise.resolve() : Promise.reject(exitCode);
+        return exitCode === 0
+          ? Promise.resolve()
+          : Promise.reject(new Error(`fx ota failed with exit code ${exitCode}`));
       }
     );
   });
diff --git a/src/workflow/index.ts b/src/workflow/index.ts
index df6c192..e67f299 100644
--- a/src/workflow/index.ts
+++ b/src/workflow/index.ts
@@ -8,14 +8,14 @@
 import * as logger from '../logger';
 import { registerCommandWithAnalyticsEvent } from '../analytics/vscode_events';
 import { registerBuildCommands } from './build';
-import { FuchsiaDiagnostics } from './problem_matcher';
+import { FuchsiaDiagnostics, type PackageJsonStub } from './problem_matcher';
 import { PkgHistory, registerClearPackageHistoryCommand, registerRemovePackageCommand, registerSetCommand, toList } from './set';
 
 /**
  * initialize fuchsia workflow interaction commands
  */
 export function setUpWorkflowInteraction(ctx: vscode.ExtensionContext, setup: Setup) {
-  const collection = new FuchsiaDiagnostics(ctx.extension.packageJSON);
+  const collection = new FuchsiaDiagnostics(ctx.extension.packageJSON as PackageJsonStub);
   const pkgHistory = new PkgHistory(ctx.workspaceState);
 
   registerBuildCommands(ctx, setup, collection);
@@ -38,7 +38,9 @@
       });
 
       const exitCode = await cmd?.exitCode;
-      return exitCode === 0 ? Promise.resolve() : Promise.reject(exitCode);
+      return exitCode === 0
+        ? Promise.resolve()
+        : Promise.reject(new Error(`fx serve failed with exit code ${exitCode}`));
 
       /** callback to handle buffer from process */
       function handleData(buffer: Buffer) {
@@ -65,7 +67,7 @@
         logger.info('Stopped the repository server', 'fx serve');
       })
       .catch((err) => {
-        logger.error(err);
+        logger.error(String(err));
       });
   });
 
diff --git a/src/workflow/problem_matcher.ts b/src/workflow/problem_matcher.ts
index 72dca69..a64c8f1 100644
--- a/src/workflow/problem_matcher.ts
+++ b/src/workflow/problem_matcher.ts
@@ -23,7 +23,7 @@
   };
 }
 
-interface PackageJsonStub {
+export interface PackageJsonStub {
   contributes: {
     problemMatchers: ProblemMatcher[];
   };
diff --git a/src/zxdb/async_backtrace.ts b/src/zxdb/async_backtrace.ts
index 6d8f11c..7a04b40 100644
--- a/src/zxdb/async_backtrace.ts
+++ b/src/zxdb/async_backtrace.ts
@@ -282,7 +282,7 @@
 
     // Assumes that the data from the DAP server conforms to the interface.
     try {
-      const threadBacktrace = new AsyncBacktrace(e.body, e.session.id);
+      const threadBacktrace = new AsyncBacktrace(e.body as AsyncBacktraceUpdateData, e.session.id);
       if (threadBacktrace.threadExists) {
         provider.updateAsyncBacktrace(threadBacktrace);
       } else {
@@ -290,7 +290,7 @@
       }
     } catch (err) {
       logger.error(
-        `Failed to update async backtrace pane from DAP server event: ${err}`,
+        `Failed to update async backtrace pane from DAP server event: ${String(err)}`,
         'async_backtrace',
       );
       logger.error(`Corresponding DAP server event: ${JSON.stringify(e.body)}`, 'async_backtrace');
diff --git a/src/zxdb/console.ts b/src/zxdb/console.ts
index 87682ca..c16879c 100644
--- a/src/zxdb/console.ts
+++ b/src/zxdb/console.ts
@@ -179,16 +179,20 @@
     try {
       logger.debug(`Starting new zxdb process connected to port ${portNumber}`, 'zxdb-console');
       this.zxdbProcess = fx.runStreaming(ZXDB_COMMAND_OPT.concat(['--debug-adapter-port', `${portNumber}`]));
+      this.zxdbProcess?.stdout.setEncoding('utf8');
+      this.zxdbProcess?.stderr.setEncoding('utf8');
       if (!this.zxdbProcess) {
         logger.warn('Cannot start zxdb, no path to ffx');
         return;
       }
 
       let errorMsg = '';
-      this.zxdbProcess.stdout.on('data', (data) => { this.processOutput(session, 'stdout', data); });
-      this.zxdbProcess.stderr.on('data', (data) => {
-        this.processOutput(session, 'stderr', data);
-        errorMsg = `${errorMsg}${data}\n`;
+      this.zxdbProcess.stdout.on('data', (data: unknown) => {
+        this.processOutput(session, 'stdout', String(data));
+      });
+      this.zxdbProcess.stderr.on('data', (data: unknown) => {
+        this.processOutput(session, 'stderr', String(data));
+        errorMsg = `${errorMsg}${String(data)}\n`;
       });
       this.zxdbProcess.on('close', (code) => {
         onCloseCallback(errorMsg);
@@ -197,7 +201,7 @@
     } catch (e) {
       logger.error('Cannot start zxdb console: ', 'zxdb-console', e);
       void vscode.window.showErrorMessage(
-        `Cannot start console: ${(e instanceof Error) ? e.message : e}`);
+        `Cannot start console: ${(e instanceof Error) ? e.message : String(e)}`);
     }
   }
 
diff --git a/src/zxdb/index.ts b/src/zxdb/index.ts
index 1c99716..38f4fd6 100644
--- a/src/zxdb/index.ts
+++ b/src/zxdb/index.ts
@@ -55,9 +55,10 @@
  */
 function setUpZxdbPrompts(ctx: vscode.ExtensionContext) {
   ctx.subscriptions.push(vscode.commands.registerCommand(
-    'fuchsia.internal.zxdb.pickProcess', config => {
+    'fuchsia.internal.zxdb.pickProcess', (config: unknown) => {
+      const cfg = config as vscode.DebugConfiguration | undefined;
       let promptText: string;
-      if (config.request === 'launch') {
+      if (cfg?.request === 'launch') {
         promptText =
           'Launch(zxdb): Enter name of the process that will be launched.' +
           ' Hint: For components, it\'s usually the component name.';
@@ -198,8 +199,8 @@
       logger.info('Creating the debug adapter client.');
       return new vscode.DebugAdapterServer(portNumber);
     } catch (err) {
-      logger.error(`${err}.\nSee ${TROUBLESHOOTING_URL}`);
-      const dap = new ErrorFakeDap(`${err}`);
+      logger.error(`${String(err)}.\nSee ${TROUBLESHOOTING_URL}`);
+      const dap = new ErrorFakeDap(`${String(err)}`);
       return new vscode.DebugAdapterInlineImplementation(dap);
     }
   }
@@ -263,9 +264,8 @@
       // backend and hence debug adapter reports connection loss errors which
       // needs to be ignored. It might confuse users if these errors are shown.
       this.ignoreError = true;
-      if (request.arguments && request.arguments.restart) {
-        this.restart = request.arguments.restart;
-      }
+      const disconnectRequest = request as { arguments?: { restart?: boolean } };
+      this.restart = !!disconnectRequest?.arguments?.restart;
     } if (request.command === 'terminate') {
       // Ignore errors reported after this. The connection is closed by the
       // backend and hence debug adapter reports connection loss errors.
@@ -385,7 +385,7 @@
   try {
     await ffx.runFfx(['component', 'stop', 'core/debugger']);
   } catch (err) {
-    logger.error(`Unable to stop existing zxdb sessions: ${err}`, 'zxdb');
+    logger.error(`Unable to stop existing zxdb sessions: ${String(err)}`, 'zxdb');
     throw err;
   }
 }
diff --git a/syntax/cml.ts b/syntax/cml.ts
index da36a20..49318e8 100644
--- a/syntax/cml.ts
+++ b/syntax/cml.ts
@@ -346,7 +346,7 @@
    * @returns NamedPattern.
    */
   word: function (word: Pattern): NamedPattern {
-    return new NamedPattern(`\\b${word}\\b`, getPatternNames(word));
+    return new NamedPattern(`\\b${word.toString()}\\b`, getPatternNames(word));
   },
 
   /**
@@ -356,7 +356,7 @@
    * @returns NamedPattern.
    */
   oneString: function (word: Pattern, type?: string): NamedPattern {
-    return patterns.named(`"${word}"`, type ?? 'string.quoted.double');
+    return patterns.named(`"${word.toString()}"`, type ?? 'string.quoted.double');
   },
 
   /**
@@ -442,7 +442,7 @@
    * @returns NamedPattern
    */
   named: function (pattern: Pattern, name: string) {
-    return new NamedPattern(`(${pattern})`, [name, ...getPatternNames(pattern)]);
+    return new NamedPattern(`(${pattern.toString()})`, [name, ...getPatternNames(pattern)]);
   },
 
   /**
diff --git a/syntax/fidl.ts b/syntax/fidl.ts
index 731b1d9..1f29fac 100644
--- a/syntax/fidl.ts
+++ b/syntax/fidl.ts
@@ -213,7 +213,7 @@
    * @returns NamedPattern.
    */
   word: function (word: Pattern): NamedPattern {
-    return new NamedPattern(`\\b${word}\\b`, getPatternNames(word));
+    return new NamedPattern(`\\b${word.toString()}\\b`, getPatternNames(word));
   },
 
   /**
@@ -241,7 +241,7 @@
    * @returns NamedPattern
    */
   zeroOrMore: function (pattern: Pattern): NamedPattern {
-    return new NamedPattern(`(?:${pattern}\\s*)*`, getPatternNames(pattern));
+    return new NamedPattern(`(?:${pattern.toString()}\\s*)*`, getPatternNames(pattern));
   },
 
   /**
@@ -250,7 +250,7 @@
    * @returns NamedPattern
    */
   optional: function (pattern: Pattern): NamedPattern {
-    return new NamedPattern(`(?:${pattern})?`, getPatternNames(pattern));
+    return new NamedPattern(`(?:${pattern.toString()})?`, getPatternNames(pattern));
   },
 
   /**
@@ -260,7 +260,7 @@
    * @returns NamedPattern
    */
   named: function (pattern: Pattern, name: string) {
-    return new NamedPattern(`(${pattern})`, [name, ...getPatternNames(pattern)]);
+    return new NamedPattern(`(${pattern.toString()})`, [name, ...getPatternNames(pattern)]);
   },
 
   /**
diff --git a/syntax/fuchsia-log.ts b/syntax/fuchsia-log.ts
index 2cbcb68..88a2b31 100644
--- a/syntax/fuchsia-log.ts
+++ b/syntax/fuchsia-log.ts
@@ -166,7 +166,7 @@
    * @returns NamedPattern
    */
   zeroOrMore: function (pattern: Pattern): NamedPattern {
-    return new NamedPattern(`(?:${pattern})*`, getPatternNames(pattern));
+    return new NamedPattern(`(?:${pattern.toString()})*`, getPatternNames(pattern));
   },
 
   /**
@@ -176,7 +176,7 @@
    * @returns NamedPattern
    */
   named: function (pattern: Pattern, name: string) {
-    return new NamedPattern(`(${pattern})`, [name, ...getPatternNames(pattern)]);
+    return new NamedPattern(`(${pattern.toString()})`, [name, ...getPatternNames(pattern)]);
   },
 };
 
@@ -188,7 +188,7 @@
 const USER_TAGS = patterns.named(patterns.zeroOrMore('\\[([^\\]]*)\\]'), 'entity.name.type');
 
 const TAGS = new NamedPattern(
-  `^${TIMESTAMP_TAG}${USER_TAGS}`,
+  `^${TIMESTAMP_TAG.toString()}${USER_TAGS.toString()}`,
   getPatternNames([TIMESTAMP_TAG, USER_TAGS])
 );