Repository: stephenh/ts-proto
Pull request #868 (2023-07-07 23:09:07)

feat: Ensure strict(er) TS compliance for the generated code
Changes:

- Add a new `tsc:check` script to ensure that there are no TS files with undetected errors.
- Split the TS configs in `integrations` so the compiled proto TS files can underly much stricter configuration requirements than the test files themselves.
- Add `DOM` to `compilerOptions.lib` for integration tests to have DOM's `AbortSignal` in scope.
- Apply the strictest TS preset there is to the compiled proto TS files (with a few exceptions that may be resolved later).
- Update the generated code to adhere to the stricter TS config.
- Modernise a few areas of the generated code.
- Add `tsc:check` to the CI script

---

There were a bunch of type errors in the integration tests that weren't caught by anything. I honestly don't know Jest enough to understand why that happens but at least the new `tsc:check` script catches them correctly.

I've applied the new `unwrap` utility in a couple of instances where I'm not precisely sure what the intended behaviour is, so this is a best effort guess. In any case, it doesn't make the experience any worse because those were uncaught error cases.

Resolving unused parameters `noUnusedParameters: true` is probably a Sisyphean task but it would be great to resolve the very few areas where there were unused locals `noUnusedLocals: true` in the future.

Note: Bumping Node types from 14 to 16 was kinda arbitrary. Node 16 is almost EOL, too.

threema-lenny: I noticed that the CI still runs Node 14. Considering it's EOL and I've bumped the Node types, should it be removed from the test matrix?
stephenh: @threema-lenny sure, I'm good dropping Node 14 support, thanks!
stephenh: Looks great @threema-lenny ! Fwiw I changed the PR title over to "feat:" to trigger a release when this merges. Going to go ahead and merge this.

Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e698cee4..69f25a99 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -12,7 +12,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [14.x, 16.x, 18.x]
+        node-version: [16.x, 18.x, 20.x]
 
     steps:
       - uses: actions/checkout@v3
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 67377b7d..d2dde29d 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -12,7 +12,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [14.x]
+        node-version: [20.x]
 
     steps:
       - uses: actions/checkout@v3
@@ -21,7 +21,16 @@ jobs:
         with:
           node-version: ${{ matrix.node-version }}
       - name: install dependencies
-        run: yarn install || echo "ignore failure"
+        run: yarn install
+      - name: Prepare Integration
+        run: ./integration/pbjs.sh
+      # This will fail if any git-tracked file has changed
+      - name: Codegen
+        run: yarn bin2ts
+      - name: TypeScript
+        run: yarn tsc:check
+        env:
+          CI: true
       - name: Prettier
         run: yarn format:check
         env:
diff --git a/integration/batching-with-context-esModuleInterop/batching.ts b/integration/batching-with-context-esModuleInterop/batching.ts
index b6a1f28e..eb760ceb 100644
--- a/integration/batching-with-context-esModuleInterop/batching.ts
+++ b/integration/batching-with-context-esModuleInterop/batching.ts
@@ -705,7 +705,7 @@ export class EntityServiceClientImpl<Context extends DataLoaders> implements Ent
       return new DataLoader<string, Entity>((ids) => {
         const request = { ids };
         return this.BatchMapQuery(ctx, request).then((res) => {
-          return ids.map((key) => res.entities[key]);
+          return ids.map((key) => res.entities[key] ?? fail());
         });
       }, { cacheKeyFn: hash, ...ctx.rpcDataLoaderOptions });
     });
@@ -770,3 +770,7 @@ function isObject(value: any): boolean {
 function isSet(value: any): boolean {
   return value !== null && value !== undefined;
 }
+
+function fail(message?: string): never {
+  throw new Error(message ?? "Failed");
+}
diff --git a/integration/batching-with-context/batching.ts b/integration/batching-with-context/batching.ts
index 6ea9a0cb..319eee66 100644
--- a/integration/batching-with-context/batching.ts
+++ b/integration/batching-with-context/batching.ts
@@ -705,7 +705,7 @@ export class EntityServiceClientImpl<Context extends DataLoaders> implements Ent
       return new DataLoader<string, Entity>((ids) => {
         const request = { ids };
         return this.BatchMapQuery(ctx, request).then((res) => {
-          return ids.map((key) => res.entities[key]);
+          return ids.map((key) => res.entities[key] ?? fail());
         });
       }, { cacheKeyFn: hash, ...ctx.rpcDataLoaderOptions });
     });
@@ -770,3 +770,7 @@ function isObject(value: any): boolean {
 function isSet(value: any): boolean {
   return value !== null && value !== undefined;
 }
+
+function fail(message?: string): never {
+  throw new Error(message ?? "Failed");
+}
diff --git a/integration/bytes-as-base64/message.ts b/integration/bytes-as-base64/message.ts
index 3b296488..50a0b580 100644
--- a/integration/bytes-as-base64/message.ts
+++ b/integration/bytes-as-base64/message.ts
@@ -33,10 +33,10 @@ export const Message = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/bytes-node/google/protobuf/wrappers.ts b/integration/bytes-node/google/protobuf/wrappers.ts
index 61fcfe42..6e5cfb4b 100644
--- a/integration/bytes-node/google/protobuf/wrappers.ts
+++ b/integration/bytes-node/google/protobuf/wrappers.ts
@@ -599,10 +599,10 @@ export const BytesValue = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/bytes-node/point.ts b/integration/bytes-node/point.ts
index 12aef40c..66421dcf 100644
--- a/integration/bytes-node/point.ts
+++ b/integration/bytes-node/point.ts
@@ -81,10 +81,10 @@ export const Point = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/file-suffix/google/protobuf/timestamp.pb.ts b/integration/file-suffix/google/protobuf/timestamp.pb.ts
index e4b5e083..dc3946fe 100644
--- a/integration/file-suffix/google/protobuf/timestamp.pb.ts
+++ b/integration/file-suffix/google/protobuf/timestamp.pb.ts
@@ -182,10 +182,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/generic-metadata/some-file.ts b/integration/generic-metadata/some-file.ts
new file mode 100644
index 00000000..2921688a
--- /dev/null
+++ b/integration/generic-metadata/some-file.ts
@@ -0,0 +1,3 @@
+export interface Foo {
+  foo: "bar";
+}
diff --git a/integration/grpc-js-use-date-false/google/protobuf/timestamp.ts b/integration/grpc-js-use-date-false/google/protobuf/timestamp.ts
index e4b5e083..dc3946fe 100644
--- a/integration/grpc-js-use-date-false/google/protobuf/timestamp.ts
+++ b/integration/grpc-js-use-date-false/google/protobuf/timestamp.ts
@@ -182,10 +182,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/grpc-js-use-date-string/google/protobuf/timestamp.ts b/integration/grpc-js-use-date-string/google/protobuf/timestamp.ts
index e4b5e083..dc3946fe 100644
--- a/integration/grpc-js-use-date-string/google/protobuf/timestamp.ts
+++ b/integration/grpc-js-use-date-string/google/protobuf/timestamp.ts
@@ -182,10 +182,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/grpc-js-use-date-true/google/protobuf/timestamp.ts b/integration/grpc-js-use-date-true/google/protobuf/timestamp.ts
index e4b5e083..dc3946fe 100644
--- a/integration/grpc-js-use-date-true/google/protobuf/timestamp.ts
+++ b/integration/grpc-js-use-date-true/google/protobuf/timestamp.ts
@@ -182,10 +182,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/grpc-js/google/protobuf/timestamp.ts b/integration/grpc-js/google/protobuf/timestamp.ts
index e4b5e083..dc3946fe 100644
--- a/integration/grpc-js/google/protobuf/timestamp.ts
+++ b/integration/grpc-js/google/protobuf/timestamp.ts
@@ -182,10 +182,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/grpc-js/google/protobuf/wrappers.ts b/integration/grpc-js/google/protobuf/wrappers.ts
index 99125575..356be214 100644
--- a/integration/grpc-js/google/protobuf/wrappers.ts
+++ b/integration/grpc-js/google/protobuf/wrappers.ts
@@ -599,10 +599,10 @@ export const BytesValue = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/grpc-web-abort-signal/example.ts b/integration/grpc-web-abort-signal/example.ts
index 6298c146..66bdb755 100644
--- a/integration/grpc-web-abort-signal/example.ts
+++ b/integration/grpc-web-abort-signal/example.ts
@@ -994,14 +994,14 @@ export class GrpcWebImpl {
     const request = { ..._request, ...methodDesc.requestType };
     const maybeCombinedMetadata = metadata && this.options.metadata
       ? new BrowserHeaders({ ...this.options?.metadata.headersMap, ...metadata?.headersMap })
-      : metadata || this.options.metadata;
+      : metadata ?? this.options.metadata;
     return new Promise((resolve, reject) => {
       const client = grpc.unary(methodDesc, {
         request,
         host: this.host,
-        metadata: maybeCombinedMetadata,
-        transport: this.options.transport,
-        debug: this.options.debug,
+        metadata: maybeCombinedMetadata ?? {},
+        ...(this.options.transport !== undefined ? { transport: this.options.transport } : {}),
+        debug: this.options.debug ?? false,
         onEnd: function (response) {
           if (response.status === grpc.Code.OK) {
             resolve(response.message!.toObject());
@@ -1027,20 +1027,21 @@ export class GrpcWebImpl {
     metadata: grpc.Metadata | undefined,
     abortSignal?: AbortSignal,
   ): Observable<any> {
-    const upStreamCodes = this.options.upStreamRetryCodes || [];
+    const upStreamCodes = this.options.upStreamRetryCodes ?? [];
     const DEFAULT_TIMEOUT_TIME: number = 3_000;
     const request = { ..._request, ...methodDesc.requestType };
+    const transport = this.options.streamingTransport ?? this.options.transport;
     const maybeCombinedMetadata = metadata && this.options.metadata
       ? new BrowserHeaders({ ...this.options?.metadata.headersMap, ...metadata?.headersMap })
-      : metadata || this.options.metadata;
+      : metadata ?? this.options.metadata;
     return new Observable((observer) => {
       const upStream = (() => {
         const client = grpc.invoke(methodDesc, {
           host: this.host,
           request,
-          transport: this.options.streamingTransport || this.options.transport,
-          metadata: maybeCombinedMetadata,
-          debug: this.options.debug,
+          ...(transport !== undefined ? { transport } : {}),
+          metadata: maybeCombinedMetadata ?? {},
+          debug: this.options.debug ?? false,
           onMessage: (next) => observer.next(next),
           onEnd: (code: grpc.Code, message: string, trailers: grpc.Metadata) => {
             if (code === 0) {
@@ -1073,10 +1074,10 @@ export class GrpcWebImpl {
   }
 }
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/grpc-web-no-streaming-observable/example.ts b/integration/grpc-web-no-streaming-observable/example.ts
index 1a6d76f3..76d2ddf8 100644
--- a/integration/grpc-web-no-streaming-observable/example.ts
+++ b/integration/grpc-web-no-streaming-observable/example.ts
@@ -441,14 +441,14 @@ export class GrpcWebImpl {
     const request = { ..._request, ...methodDesc.requestType };
     const maybeCombinedMetadata = metadata && this.options.metadata
       ? new BrowserHeaders({ ...this.options?.metadata.headersMap, ...metadata?.headersMap })
-      : metadata || this.options.metadata;
+      : metadata ?? this.options.metadata;
     return new Observable((observer) => {
       grpc.unary(methodDesc, {
         request,
         host: this.host,
-        metadata: maybeCombinedMetadata,
-        transport: this.options.transport,
-        debug: this.options.debug,
+        metadata: maybeCombinedMetadata ?? {},
+        ...(this.options.transport !== undefined ? { transport: this.options.transport } : {}),
+        debug: this.options.debug ?? false,
         onEnd: (next) => {
           if (next.status !== 0) {
             const err = new GrpcWebError(next.statusMessage, next.status, next.trailers);
@@ -463,10 +463,10 @@ export class GrpcWebImpl {
   }
 }
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/grpc-web-no-streaming/example.ts b/integration/grpc-web-no-streaming/example.ts
index 528aa702..205d0a72 100644
--- a/integration/grpc-web-no-streaming/example.ts
+++ b/integration/grpc-web-no-streaming/example.ts
@@ -439,14 +439,14 @@ export class GrpcWebImpl {
     const request = { ..._request, ...methodDesc.requestType };
     const maybeCombinedMetadata = metadata && this.options.metadata
       ? new BrowserHeaders({ ...this.options?.metadata.headersMap, ...metadata?.headersMap })
-      : metadata || this.options.metadata;
+      : metadata ?? this.options.metadata;
     return new Promise((resolve, reject) => {
       grpc.unary(methodDesc, {
         request,
         host: this.host,
-        metadata: maybeCombinedMetadata,
-        transport: this.options.transport,
-        debug: this.options.debug,
+        metadata: maybeCombinedMetadata ?? {},
+        ...(this.options.transport !== undefined ? { transport: this.options.transport } : {}),
+        debug: this.options.debug ?? false,
         onEnd: function (response) {
           if (response.status === grpc.Code.OK) {
             resolve(response.message!.toObject());
@@ -460,10 +460,10 @@ export class GrpcWebImpl {
   }
 }
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/grpc-web/client-ts.ts b/integration/grpc-web/client-ts.ts
index 5781306c..bc3c1da1 100755
--- a/integration/grpc-web/client-ts.ts
+++ b/integration/grpc-web/client-ts.ts
@@ -31,7 +31,7 @@ async function main() {
   try {
     await creds.Update({ description: 'test desc2' });
   } catch (e) {
-    console.log('got expected error', e.message);
+    console.log('got expected error', e);
   }
 
   const obs = client.ActiveUserSettingsStream({});
diff --git a/integration/grpc-web/example.ts b/integration/grpc-web/example.ts
index 5d52d6f5..91406139 100644
--- a/integration/grpc-web/example.ts
+++ b/integration/grpc-web/example.ts
@@ -949,14 +949,14 @@ export class GrpcWebImpl {
     const request = { ..._request, ...methodDesc.requestType };
     const maybeCombinedMetadata = metadata && this.options.metadata
       ? new BrowserHeaders({ ...this.options?.metadata.headersMap, ...metadata?.headersMap })
-      : metadata || this.options.metadata;
+      : metadata ?? this.options.metadata;
     return new Promise((resolve, reject) => {
       grpc.unary(methodDesc, {
         request,
         host: this.host,
-        metadata: maybeCombinedMetadata,
-        transport: this.options.transport,
-        debug: this.options.debug,
+        metadata: maybeCombinedMetadata ?? {},
+        ...(this.options.transport !== undefined ? { transport: this.options.transport } : {}),
+        debug: this.options.debug ?? false,
         onEnd: function (response) {
           if (response.status === grpc.Code.OK) {
             resolve(response.message!.toObject());
@@ -974,20 +974,21 @@ export class GrpcWebImpl {
     _request: any,
     metadata: grpc.Metadata | undefined,
   ): Observable<any> {
-    const upStreamCodes = this.options.upStreamRetryCodes || [];
+    const upStreamCodes = this.options.upStreamRetryCodes ?? [];
     const DEFAULT_TIMEOUT_TIME: number = 3_000;
     const request = { ..._request, ...methodDesc.requestType };
+    const transport = this.options.streamingTransport ?? this.options.transport;
     const maybeCombinedMetadata = metadata && this.options.metadata
       ? new BrowserHeaders({ ...this.options?.metadata.headersMap, ...metadata?.headersMap })
-      : metadata || this.options.metadata;
+      : metadata ?? this.options.metadata;
     return new Observable((observer) => {
       const upStream = (() => {
         const client = grpc.invoke(methodDesc, {
           host: this.host,
           request,
-          transport: this.options.streamingTransport || this.options.transport,
-          metadata: maybeCombinedMetadata,
-          debug: this.options.debug,
+          ...(transport !== undefined ? { transport } : {}),
+          metadata: maybeCombinedMetadata ?? {},
+          debug: this.options.debug ?? false,
           onMessage: (next) => observer.next(next),
           onEnd: (code: grpc.Code, message: string, trailers: grpc.Metadata) => {
             if (code === 0) {
@@ -1011,10 +1012,10 @@ export class GrpcWebImpl {
   }
 }
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/import-mapping/google/protobuf/timestamp.ts b/integration/import-mapping/google/protobuf/timestamp.ts
index e4b5e083..dc3946fe 100644
--- a/integration/import-mapping/google/protobuf/timestamp.ts
+++ b/integration/import-mapping/google/protobuf/timestamp.ts
@@ -182,10 +182,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/import-suffix/google/protobuf/timestamp.pb.ts b/integration/import-suffix/google/protobuf/timestamp.pb.ts
index cc89db58..64e5e644 100644
--- a/integration/import-suffix/google/protobuf/timestamp.pb.ts
+++ b/integration/import-suffix/google/protobuf/timestamp.pb.ts
@@ -182,10 +182,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/meta-typings/google/protobuf/timestamp.ts b/integration/meta-typings/google/protobuf/timestamp.ts
index 0bb6fcf2..476e3768 100644
--- a/integration/meta-typings/google/protobuf/timestamp.ts
+++ b/integration/meta-typings/google/protobuf/timestamp.ts
@@ -278,10 +278,10 @@ export const protoMetadata: ProtoMetadata = {
   dependencies: [],
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/meta-typings/google/protobuf/wrappers.ts b/integration/meta-typings/google/protobuf/wrappers.ts
index b2d0aeb3..21c5791a 100644
--- a/integration/meta-typings/google/protobuf/wrappers.ts
+++ b/integration/meta-typings/google/protobuf/wrappers.ts
@@ -817,10 +817,10 @@ export const protoMetadata: ProtoMetadata = {
   dependencies: [],
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/meta-typings/simple.ts b/integration/meta-typings/simple.ts
index 5602bb82..8531912c 100644
--- a/integration/meta-typings/simple.ts
+++ b/integration/meta-typings/simple.ts
@@ -2950,10 +2950,10 @@ export const protoMetadata: ProtoMetadata = {
   dependencies: [protoMetadata1, protoMetadata2, protoMetadata3, protoMetadata4],
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/nice-grpc/google/protobuf/timestamp.ts b/integration/nice-grpc/google/protobuf/timestamp.ts
index bbf01134..4dea0b96 100644
--- a/integration/nice-grpc/google/protobuf/timestamp.ts
+++ b/integration/nice-grpc/google/protobuf/timestamp.ts
@@ -182,10 +182,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/nice-grpc/google/protobuf/wrappers.ts b/integration/nice-grpc/google/protobuf/wrappers.ts
index 1387d5f3..c1ea99fa 100644
--- a/integration/nice-grpc/google/protobuf/wrappers.ts
+++ b/integration/nice-grpc/google/protobuf/wrappers.ts
@@ -599,10 +599,10 @@ export const BytesValue = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/oneof-properties/oneof.ts b/integration/oneof-properties/oneof.ts
index 9ad527f5..32683549 100644
--- a/integration/oneof-properties/oneof.ts
+++ b/integration/oneof-properties/oneof.ts
@@ -337,10 +337,10 @@ export const PleaseChoose_Submessage = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/oneof-unions-snake/google/protobuf/struct.ts b/integration/oneof-unions-snake/google/protobuf/struct.ts
index a2164b32..0c168958 100644
--- a/integration/oneof-unions-snake/google/protobuf/struct.ts
+++ b/integration/oneof-unions-snake/google/protobuf/struct.ts
@@ -72,7 +72,8 @@ export interface Value {
     | { $case: "string_value"; string_value: string }
     | { $case: "bool_value"; bool_value: boolean }
     | { $case: "struct_value"; struct_value: { [key: string]: any } | undefined }
-    | { $case: "list_value"; list_value: Array<any> | undefined };
+    | { $case: "list_value"; list_value: Array<any> | undefined }
+    | undefined;
 }
 
 /**
diff --git a/integration/oneof-unions/google/protobuf/struct.ts b/integration/oneof-unions/google/protobuf/struct.ts
index 1f50b37e..3776f763 100644
--- a/integration/oneof-unions/google/protobuf/struct.ts
+++ b/integration/oneof-unions/google/protobuf/struct.ts
@@ -72,7 +72,8 @@ export interface Value {
     | { $case: "stringValue"; stringValue: string }
     | { $case: "boolValue"; boolValue: boolean }
     | { $case: "structValue"; structValue: { [key: string]: any } | undefined }
-    | { $case: "listValue"; listValue: Array<any> | undefined };
+    | { $case: "listValue"; listValue: Array<any> | undefined }
+    | undefined;
 }
 
 /**
diff --git a/integration/oneof-unions/oneof.ts b/integration/oneof-unions/oneof.ts
index ef44f16d..10f9a8c7 100644
--- a/integration/oneof-unions/oneof.ts
+++ b/integration/oneof-unions/oneof.ts
@@ -12,12 +12,13 @@ export interface PleaseChoose {
     | { $case: "aMessage"; aMessage: PleaseChoose_Submessage }
     | { $case: "aBool"; aBool: boolean }
     | { $case: "bunchaBytes"; bunchaBytes: Uint8Array }
-    | { $case: "anEnum"; anEnum: PleaseChoose_StateEnum };
+    | { $case: "anEnum"; anEnum: PleaseChoose_StateEnum }
+    | undefined;
   age: number;
   eitherOr?: { $case: "either"; either: string } | { $case: "or"; or: string } | {
     $case: "thirdOption";
     thirdOption: string;
-  };
+  } | undefined;
   signature: Uint8Array;
   value: any | undefined;
 }
@@ -463,10 +464,10 @@ export const SimpleButOptional = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/options/google/protobuf/descriptor.ts b/integration/options/google/protobuf/descriptor.ts
index 039e186a..112bb593 100644
--- a/integration/options/google/protobuf/descriptor.ts
+++ b/integration/options/google/protobuf/descriptor.ts
@@ -5782,10 +5782,10 @@ export const protoMetadata: ProtoMetadata = {
   dependencies: [],
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/output-decode-only/google/protobuf/wrappers.ts b/integration/output-decode-only/google/protobuf/wrappers.ts
index 0cb3d044..d05f90e8 100644
--- a/integration/output-decode-only/google/protobuf/wrappers.ts
+++ b/integration/output-decode-only/google/protobuf/wrappers.ts
@@ -355,10 +355,10 @@ export const BytesValue = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/remove-enum-prefix/remove-enum-prefix.ts b/integration/remove-enum-prefix/remove-enum-prefix.ts
index 5542cf29..c243421b 100644
--- a/integration/remove-enum-prefix/remove-enum-prefix.ts
+++ b/integration/remove-enum-prefix/remove-enum-prefix.ts
@@ -72,10 +72,10 @@ export function barToJSON(object: Bar): string {
   }
 }
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple-esmodule-interop/simple.ts b/integration/simple-esmodule-interop/simple.ts
index 6a8f5b13..422d6ec0 100644
--- a/integration/simple-esmodule-interop/simple.ts
+++ b/integration/simple-esmodule-interop/simple.ts
@@ -306,10 +306,10 @@ export const Numbers = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple-json-name/google/protobuf/timestamp.ts b/integration/simple-json-name/google/protobuf/timestamp.ts
index e4b5e083..dc3946fe 100644
--- a/integration/simple-json-name/google/protobuf/timestamp.ts
+++ b/integration/simple-json-name/google/protobuf/timestamp.ts
@@ -182,10 +182,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple-long-bigint/google/protobuf/wrappers.ts b/integration/simple-long-bigint/google/protobuf/wrappers.ts
index f493e2de..613a75aa 100644
--- a/integration/simple-long-bigint/google/protobuf/wrappers.ts
+++ b/integration/simple-long-bigint/google/protobuf/wrappers.ts
@@ -599,10 +599,10 @@ export const BytesValue = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple-long-string/google/protobuf/wrappers.ts b/integration/simple-long-string/google/protobuf/wrappers.ts
index 42e990ef..a69f21c2 100644
--- a/integration/simple-long-string/google/protobuf/wrappers.ts
+++ b/integration/simple-long-string/google/protobuf/wrappers.ts
@@ -599,10 +599,10 @@ export const BytesValue = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple-long/google/protobuf/wrappers.ts b/integration/simple-long/google/protobuf/wrappers.ts
index 9269f087..3347b40d 100644
--- a/integration/simple-long/google/protobuf/wrappers.ts
+++ b/integration/simple-long/google/protobuf/wrappers.ts
@@ -599,10 +599,10 @@ export const BytesValue = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple-optionals/google/protobuf/timestamp.ts b/integration/simple-optionals/google/protobuf/timestamp.ts
index e4b5e083..dc3946fe 100644
--- a/integration/simple-optionals/google/protobuf/timestamp.ts
+++ b/integration/simple-optionals/google/protobuf/timestamp.ts
@@ -182,10 +182,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple-optionals/google/protobuf/wrappers.ts b/integration/simple-optionals/google/protobuf/wrappers.ts
index 99125575..356be214 100644
--- a/integration/simple-optionals/google/protobuf/wrappers.ts
+++ b/integration/simple-optionals/google/protobuf/wrappers.ts
@@ -599,10 +599,10 @@ export const BytesValue = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple-optionals/import_dir/thing.ts b/integration/simple-optionals/import_dir/thing.ts
index 34d8a553..231c9e09 100644
--- a/integration/simple-optionals/import_dir/thing.ts
+++ b/integration/simple-optionals/import_dir/thing.ts
@@ -5,7 +5,7 @@ import { Timestamp } from "../google/protobuf/timestamp";
 export const protobufPackage = "simple";
 
 export interface ImportedThing {
-  createdAt?: Date;
+  createdAt?: Date | undefined;
 }
 
 function createBaseImportedThing(): ImportedThing {
diff --git a/integration/simple-optionals/simple.ts b/integration/simple-optionals/simple.ts
index edf6bf89..4a3224c7 100644
--- a/integration/simple-optionals/simple.ts
+++ b/integration/simple-optionals/simple.ts
@@ -58,15 +58,15 @@ export interface Simple {
   /** Age */
   age: number;
   /** This comment will also attach */
-  createdAt?: Date;
-  child?: Child;
+  createdAt?: Date | undefined;
+  child?: Child | undefined;
   state: StateEnum;
   grandChildren: Child[];
   coins: number[];
   snacks: string[];
   oldStates: StateEnum[];
   /** A thing (imported from thing) */
-  thing?: ImportedThing;
+  thing?: ImportedThing | undefined;
 }
 
 export interface Child {
@@ -115,7 +115,7 @@ export function child_TypeToJSON(object: Child_Type): string {
 
 export interface Nested {
   name: string;
-  message?: Nested_InnerMessage;
+  message?: Nested_InnerMessage | undefined;
   state: Nested_InnerEnum;
 }
 
@@ -161,7 +161,7 @@ export function nested_InnerEnumToJSON(object: Nested_InnerEnum): string {
 /** Comment for a nested message * / */
 export interface Nested_InnerMessage {
   name: string;
-  deep?: Nested_InnerMessage_DeepMessage;
+  deep?: Nested_InnerMessage_DeepMessage | undefined;
 }
 
 export interface Nested_InnerMessage_DeepMessage {
@@ -174,9 +174,9 @@ export interface OneOfMessage {
 }
 
 export interface SimpleWithWrappers {
-  name?: string;
-  age?: number;
-  enabled?: boolean;
+  name?: string | undefined;
+  age?: number | undefined;
+  enabled?: boolean | undefined;
   coins: number[];
   snacks: string[];
 }
@@ -193,7 +193,7 @@ export interface SimpleWithMap {
 
 export interface SimpleWithMap_EntitiesByIdEntry {
   key: number;
-  value?: Entity;
+  value?: Entity | undefined;
 }
 
 export interface SimpleWithMap_NameLookupEntry {
@@ -212,7 +212,7 @@ export interface SimpleWithSnakeCaseMap {
 
 export interface SimpleWithSnakeCaseMap_EntitiesByIdEntry {
   key: number;
-  value?: Entity;
+  value?: Entity | undefined;
 }
 
 export interface PingRequest {
@@ -1869,10 +1869,10 @@ interface Rpc {
   request(service: string, method: string, data: Uint8Array): Promise<Uint8Array>;
 }
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple-optionals/thing.ts b/integration/simple-optionals/thing.ts
index 28773c45..e8529c1f 100644
--- a/integration/simple-optionals/thing.ts
+++ b/integration/simple-optionals/thing.ts
@@ -5,7 +5,7 @@ import { Timestamp } from "./google/protobuf/timestamp";
 export const protobufPackage = "simple";
 
 export interface ImportedThing {
-  createdAt?: Date;
+  createdAt?: Date | undefined;
 }
 
 function createBaseImportedThing(): ImportedThing {
diff --git a/integration/simple-prototype-defaults/google/protobuf/timestamp.ts b/integration/simple-prototype-defaults/google/protobuf/timestamp.ts
index b1ca467d..e056b52e 100644
--- a/integration/simple-prototype-defaults/google/protobuf/timestamp.ts
+++ b/integration/simple-prototype-defaults/google/protobuf/timestamp.ts
@@ -182,10 +182,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple-prototype-defaults/google/protobuf/wrappers.ts b/integration/simple-prototype-defaults/google/protobuf/wrappers.ts
index 83312667..7c1dd071 100644
--- a/integration/simple-prototype-defaults/google/protobuf/wrappers.ts
+++ b/integration/simple-prototype-defaults/google/protobuf/wrappers.ts
@@ -599,10 +599,10 @@ export const BytesValue = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple-prototype-defaults/simple.ts b/integration/simple-prototype-defaults/simple.ts
index 48504179..7963712f 100644
--- a/integration/simple-prototype-defaults/simple.ts
+++ b/integration/simple-prototype-defaults/simple.ts
@@ -2796,10 +2796,10 @@ interface Rpc {
   request(service: string, method: string, data: Uint8Array): Promise<Uint8Array>;
 }
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple-snake/google/protobuf/timestamp.ts b/integration/simple-snake/google/protobuf/timestamp.ts
index e4b5e083..dc3946fe 100644
--- a/integration/simple-snake/google/protobuf/timestamp.ts
+++ b/integration/simple-snake/google/protobuf/timestamp.ts
@@ -182,10 +182,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple-snake/google/protobuf/wrappers.ts b/integration/simple-snake/google/protobuf/wrappers.ts
index 99125575..356be214 100644
--- a/integration/simple-snake/google/protobuf/wrappers.ts
+++ b/integration/simple-snake/google/protobuf/wrappers.ts
@@ -599,10 +599,10 @@ export const BytesValue = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple-snake/simple.ts b/integration/simple-snake/simple.ts
index ac4c2252..f8135112 100644
--- a/integration/simple-snake/simple.ts
+++ b/integration/simple-snake/simple.ts
@@ -1930,10 +1930,10 @@ interface Rpc {
   request(service: string, method: string, data: Uint8Array): Promise<Uint8Array>;
 }
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple-unrecognized-enum/google/protobuf/timestamp.ts b/integration/simple-unrecognized-enum/google/protobuf/timestamp.ts
index e4b5e083..dc3946fe 100644
--- a/integration/simple-unrecognized-enum/google/protobuf/timestamp.ts
+++ b/integration/simple-unrecognized-enum/google/protobuf/timestamp.ts
@@ -182,10 +182,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple-unrecognized-enum/google/protobuf/wrappers.ts b/integration/simple-unrecognized-enum/google/protobuf/wrappers.ts
index 99125575..356be214 100644
--- a/integration/simple-unrecognized-enum/google/protobuf/wrappers.ts
+++ b/integration/simple-unrecognized-enum/google/protobuf/wrappers.ts
@@ -599,10 +599,10 @@ export const BytesValue = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple-unrecognized-enum/simple.ts b/integration/simple-unrecognized-enum/simple.ts
index fb4ff849..bf8f3c9e 100644
--- a/integration/simple-unrecognized-enum/simple.ts
+++ b/integration/simple-unrecognized-enum/simple.ts
@@ -1857,10 +1857,10 @@ interface Rpc {
   request(service: string, method: string, data: Uint8Array): Promise<Uint8Array>;
 }
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple/google/protobuf/timestamp.ts b/integration/simple/google/protobuf/timestamp.ts
index e4b5e083..dc3946fe 100644
--- a/integration/simple/google/protobuf/timestamp.ts
+++ b/integration/simple/google/protobuf/timestamp.ts
@@ -182,10 +182,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple/google/protobuf/wrappers.ts b/integration/simple/google/protobuf/wrappers.ts
index 99125575..356be214 100644
--- a/integration/simple/google/protobuf/wrappers.ts
+++ b/integration/simple/google/protobuf/wrappers.ts
@@ -599,10 +599,10 @@ export const BytesValue = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/simple/simple.ts b/integration/simple/simple.ts
index ed5fa6b3..3a599c33 100644
--- a/integration/simple/simple.ts
+++ b/integration/simple/simple.ts
@@ -2799,10 +2799,10 @@ interface Rpc {
   request(service: string, method: string, data: Uint8Array): Promise<Uint8Array>;
 }
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/static-only-type-registry/google/protobuf/timestamp.ts b/integration/static-only-type-registry/google/protobuf/timestamp.ts
index 1630303b..ac2ec05f 100644
--- a/integration/static-only-type-registry/google/protobuf/timestamp.ts
+++ b/integration/static-only-type-registry/google/protobuf/timestamp.ts
@@ -187,10 +187,10 @@ export const Timestamp = {
 
 messageTypeRegistry.set(Timestamp.$type, Timestamp);
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/static-only/google/protobuf/timestamp.ts b/integration/static-only/google/protobuf/timestamp.ts
index 59fdfd02..1095ff43 100644
--- a/integration/static-only/google/protobuf/timestamp.ts
+++ b/integration/static-only/google/protobuf/timestamp.ts
@@ -184,10 +184,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/tsconfig.json b/integration/tsconfig.json
index 118aad9a..8e5407fb 100644
--- a/integration/tsconfig.json
+++ b/integration/tsconfig.json
@@ -1,12 +1,15 @@
 {
+  "extends": "../tsconfig.json",
   "compilerOptions": {
-    "target": "es2018",
-    "lib": ["es2018"],
-    "module": "commonjs",
-    "strict": true,
-    "outDir": "build",
-    "skipLibCheck": true,
-    "experimentalDecorators": true
+    "lib": ["es2018", "DOM"],
   },
-  "include": ["./"]
-}
+  "include": [
+    "**/*-test.ts"
+  ],
+  "exclude": [
+    "angular",
+    "batching-with-context-esModuleInterop",
+    "import-mapping/mapping.ts",
+    "simple-esmodule-interop"
+  ]
+}
\ No newline at end of file
diff --git a/integration/tsconfig.proto.json b/integration/tsconfig.proto.json
new file mode 100644
index 00000000..842413ac
--- /dev/null
+++ b/integration/tsconfig.proto.json
@@ -0,0 +1,20 @@
+{
+  "extends": ["../tsconfig.json", "@tsconfig/strictest/tsconfig.json"],
+  "compilerOptions": {
+    "lib": ["es2018", "DOM"],
+    "esModuleInterop": false,
+    "noUnusedParameters": false,
+    "noUnusedLocals": false
+  },
+  "include": [
+    "**/*.ts"
+  ],
+  "exclude": [
+    "**/*-test.ts",
+    "codegen.ts",
+    "angular",
+    "batching-with-context-esModuleInterop",
+    "import-mapping/mapping.ts",
+    "simple-esmodule-interop"
+  ]
+}
\ No newline at end of file
diff --git a/integration/type-annotations/google/protobuf/timestamp.ts b/integration/type-annotations/google/protobuf/timestamp.ts
index ebe4a8b4..8dcb63d9 100644
--- a/integration/type-annotations/google/protobuf/timestamp.ts
+++ b/integration/type-annotations/google/protobuf/timestamp.ts
@@ -186,10 +186,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/type-registry/google/protobuf/timestamp.ts b/integration/type-registry/google/protobuf/timestamp.ts
index 9f31d731..b97b50a4 100644
--- a/integration/type-registry/google/protobuf/timestamp.ts
+++ b/integration/type-registry/google/protobuf/timestamp.ts
@@ -189,10 +189,10 @@ export const Timestamp = {
 
 messageTypeRegistry.set(Timestamp.$type, Timestamp);
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/unknown-fields/google/protobuf/compiler/plugin.ts b/integration/unknown-fields/google/protobuf/compiler/plugin.ts
index d66e8bb2..38071a47 100644
--- a/integration/unknown-fields/google/protobuf/compiler/plugin.ts
+++ b/integration/unknown-fields/google/protobuf/compiler/plugin.ts
@@ -15,7 +15,7 @@ export interface Version {
    * be empty for mainline stable releases.
    */
   suffix: string;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 /** An encoded CodeGeneratorRequest is written to the plugin's stdin. */
@@ -47,7 +47,7 @@ export interface CodeGeneratorRequest {
   protoFile: FileDescriptorProto[];
   /** The version number of protocol compiler. */
   compilerVersion: Version | undefined;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 /** The plugin writes an encoded CodeGeneratorResponse to stdout. */
@@ -69,7 +69,7 @@ export interface CodeGeneratorResponse {
    */
   supportedFeatures: number;
   file: CodeGeneratorResponse_File[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 /** Sync with code_generator.h. */
@@ -143,7 +143,7 @@ export interface CodeGeneratorResponse_File {
    * into the code generation metadata for the generated files.
    */
   generatedCodeInfo: GeneratedCodeInfo | undefined;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 function createBaseVersion(): Version {
@@ -165,8 +165,7 @@ export const Version = {
       writer.uint32(34).string(message.suffix);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -255,8 +254,7 @@ export const CodeGeneratorRequest = {
       Version.encode(message.compilerVersion, writer.uint32(26).fork()).ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -342,8 +340,7 @@ export const CodeGeneratorResponse = {
       CodeGeneratorResponse_File.encode(v!, writer.uint32(122).fork()).ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -425,8 +422,7 @@ export const CodeGeneratorResponse_File = {
       GeneratedCodeInfo.encode(message.generatedCodeInfo, writer.uint32(130).fork()).ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -496,10 +492,10 @@ export const CodeGeneratorResponse_File = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/unknown-fields/google/protobuf/descriptor.ts b/integration/unknown-fields/google/protobuf/descriptor.ts
index 3364f521..835ca15e 100644
--- a/integration/unknown-fields/google/protobuf/descriptor.ts
+++ b/integration/unknown-fields/google/protobuf/descriptor.ts
@@ -10,7 +10,7 @@ export const protobufPackage = "google.protobuf";
  */
 export interface FileDescriptorSet {
   file: FileDescriptorProto[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 /** Describes a complete .proto file. */
@@ -50,7 +50,7 @@ export interface FileDescriptorProto {
    * The supported values are "proto2" and "proto3".
    */
   syntax: string;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 /** Describes a message type. */
@@ -69,7 +69,7 @@ export interface DescriptorProto {
    * A given name may only be reserved once.
    */
   reservedName: string[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface DescriptorProto_ExtensionRange {
@@ -78,7 +78,7 @@ export interface DescriptorProto_ExtensionRange {
   /** Exclusive. */
   end: number;
   options: ExtensionRangeOptions | undefined;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 /**
@@ -91,13 +91,13 @@ export interface DescriptorProto_ReservedRange {
   start: number;
   /** Exclusive. */
   end: number;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface ExtensionRangeOptions {
   /** The parser stores options it doesn't recognize here. See above. */
   uninterpretedOption: UninterpretedOption[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 /** Describes a field within a message. */
@@ -170,7 +170,7 @@ export interface FieldDescriptorProto {
    * optional with `LABEL_OPTIONAL`.
    */
   proto3Optional: boolean;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export enum FieldDescriptorProto_Type {
@@ -229,7 +229,7 @@ export enum FieldDescriptorProto_Label {
 export interface OneofDescriptorProto {
   name: string;
   options: OneofOptions | undefined;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 /** Describes an enum type. */
@@ -250,7 +250,7 @@ export interface EnumDescriptorProto {
    * be reserved once.
    */
   reservedName: string[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 /**
@@ -266,7 +266,7 @@ export interface EnumDescriptorProto_EnumReservedRange {
   start: number;
   /** Inclusive. */
   end: number;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 /** Describes a value within an enum. */
@@ -274,7 +274,7 @@ export interface EnumValueDescriptorProto {
   name: string;
   number: number;
   options: EnumValueOptions | undefined;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 /** Describes a service. */
@@ -282,7 +282,7 @@ export interface ServiceDescriptorProto {
   name: string;
   method: MethodDescriptorProto[];
   options: ServiceOptions | undefined;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 /** Describes a method of a service. */
@@ -301,7 +301,7 @@ export interface MethodDescriptorProto {
   clientStreaming: boolean;
   /** Identifies if server streams multiple server messages */
   serverStreaming: boolean;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface FileOptions {
@@ -423,7 +423,7 @@ export interface FileOptions {
    * See the documentation for the "Options" section above.
    */
   uninterpretedOption: UninterpretedOption[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 /** Generated classes can be optimized for speed or code size. */
@@ -498,7 +498,7 @@ export interface MessageOptions {
   mapEntry: boolean;
   /** The parser stores options it doesn't recognize here. See above. */
   uninterpretedOption: UninterpretedOption[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface FieldOptions {
@@ -572,7 +572,7 @@ export interface FieldOptions {
   weak: boolean;
   /** The parser stores options it doesn't recognize here. See above. */
   uninterpretedOption: UninterpretedOption[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export enum FieldOptions_CType {
@@ -596,7 +596,7 @@ export enum FieldOptions_JSType {
 export interface OneofOptions {
   /** The parser stores options it doesn't recognize here. See above. */
   uninterpretedOption: UninterpretedOption[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface EnumOptions {
@@ -614,7 +614,7 @@ export interface EnumOptions {
   deprecated: boolean;
   /** The parser stores options it doesn't recognize here. See above. */
   uninterpretedOption: UninterpretedOption[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface EnumValueOptions {
@@ -627,7 +627,7 @@ export interface EnumValueOptions {
   deprecated: boolean;
   /** The parser stores options it doesn't recognize here. See above. */
   uninterpretedOption: UninterpretedOption[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface ServiceOptions {
@@ -640,7 +640,7 @@ export interface ServiceOptions {
   deprecated: boolean;
   /** The parser stores options it doesn't recognize here. See above. */
   uninterpretedOption: UninterpretedOption[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface MethodOptions {
@@ -654,7 +654,7 @@ export interface MethodOptions {
   idempotencyLevel: MethodOptions_IdempotencyLevel;
   /** The parser stores options it doesn't recognize here. See above. */
   uninterpretedOption: UninterpretedOption[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 /**
@@ -691,7 +691,7 @@ export interface UninterpretedOption {
   doubleValue: number;
   stringValue: Uint8Array;
   aggregateValue: string;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 /**
@@ -704,7 +704,7 @@ export interface UninterpretedOption {
 export interface UninterpretedOption_NamePart {
   namePart: string;
   isExtension: boolean;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 /**
@@ -758,7 +758,7 @@ export interface SourceCodeInfo {
    *   be recorded in the future.
    */
   location: SourceCodeInfo_Location[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface SourceCodeInfo_Location {
@@ -848,7 +848,7 @@ export interface SourceCodeInfo_Location {
   leadingComments: string;
   trailingComments: string;
   leadingDetachedComments: string[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 /**
@@ -862,7 +862,7 @@ export interface GeneratedCodeInfo {
    * of its generating .proto file.
    */
   annotation: GeneratedCodeInfo_Annotation[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface GeneratedCodeInfo_Annotation {
@@ -884,7 +884,7 @@ export interface GeneratedCodeInfo_Annotation {
    * the last relevant byte (so the length of the text = end - begin).
    */
   end: number;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 function createBaseFileDescriptorSet(): FileDescriptorSet {
@@ -897,8 +897,7 @@ export const FileDescriptorSet = {
       FileDescriptorProto.encode(v!, writer.uint32(10).fork()).ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -1008,8 +1007,7 @@ export const FileDescriptorProto = {
       writer.uint32(98).string(message.syntax);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -1204,8 +1202,7 @@ export const DescriptorProto = {
       writer.uint32(82).string(v!);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -1333,8 +1330,7 @@ export const DescriptorProto_ExtensionRange = {
       ExtensionRangeOptions.encode(message.options, writer.uint32(26).fork()).ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -1410,8 +1406,7 @@ export const DescriptorProto_ReservedRange = {
       writer.uint32(16).int32(message.end);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -1477,8 +1472,7 @@ export const ExtensionRangeOptions = {
       UninterpretedOption.encode(v!, writer.uint32(7994).fork()).ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -1580,8 +1574,7 @@ export const FieldDescriptorProto = {
       writer.uint32(136).bool(message.proto3Optional);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -1713,8 +1706,7 @@ export const OneofDescriptorProto = {
       OneofOptions.encode(message.options, writer.uint32(18).fork()).ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -1792,8 +1784,7 @@ export const EnumDescriptorProto = {
       writer.uint32(42).string(v!);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -1883,8 +1874,7 @@ export const EnumDescriptorProto_EnumReservedRange = {
       writer.uint32(16).int32(message.end);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -1956,8 +1946,7 @@ export const EnumValueDescriptorProto = {
       EnumValueOptions.encode(message.options, writer.uint32(26).fork()).ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -2036,8 +2025,7 @@ export const ServiceDescriptorProto = {
       ServiceOptions.encode(message.options, writer.uint32(26).fork()).ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -2133,8 +2121,7 @@ export const MethodDescriptorProto = {
       writer.uint32(48).bool(message.serverStreaming);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -2311,8 +2298,7 @@ export const FileOptions = {
       UninterpretedOption.encode(v!, writer.uint32(7994).fork()).ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -2530,8 +2516,7 @@ export const MessageOptions = {
       UninterpretedOption.encode(v!, writer.uint32(7994).fork()).ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -2645,8 +2630,7 @@ export const FieldOptions = {
       UninterpretedOption.encode(v!, writer.uint32(7994).fork()).ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -2747,8 +2731,7 @@ export const OneofOptions = {
       UninterpretedOption.encode(v!, writer.uint32(7994).fork()).ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -2813,8 +2796,7 @@ export const EnumOptions = {
       UninterpretedOption.encode(v!, writer.uint32(7994).fork()).ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -2890,8 +2872,7 @@ export const EnumValueOptions = {
       UninterpretedOption.encode(v!, writer.uint32(7994).fork()).ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -2960,8 +2941,7 @@ export const ServiceOptions = {
       UninterpretedOption.encode(v!, writer.uint32(7994).fork()).ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -3033,8 +3013,7 @@ export const MethodOptions = {
       UninterpretedOption.encode(v!, writer.uint32(7994).fork()).ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -3134,8 +3113,7 @@ export const UninterpretedOption = {
       writer.uint32(66).string(message.aggregateValue);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -3239,8 +3217,7 @@ export const UninterpretedOption_NamePart = {
       writer.uint32(16).bool(message.isExtension);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -3306,8 +3283,7 @@ export const SourceCodeInfo = {
       SourceCodeInfo_Location.encode(v!, writer.uint32(10).fork()).ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -3389,8 +3365,7 @@ export const SourceCodeInfo_Location = {
       writer.uint32(50).string(v!);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -3497,8 +3472,7 @@ export const GeneratedCodeInfo = {
       GeneratedCodeInfo_Annotation.encode(v!, writer.uint32(10).fork()).ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -3568,8 +3542,7 @@ export const GeneratedCodeInfo_Annotation = {
       writer.uint32(32).int32(message.end);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -3649,10 +3622,10 @@ export const GeneratedCodeInfo_Annotation = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/unknown-fields/options.ts b/integration/unknown-fields/options.ts
index a11f4214..4f2081e9 100644
--- a/integration/unknown-fields/options.ts
+++ b/integration/unknown-fields/options.ts
@@ -14,15 +14,15 @@ export interface MyMessage {
   foo2?: number | undefined;
   bar?: string | undefined;
   quux?: string | undefined;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface RequestType {
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface ResponseType {
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 function createBaseMyMessage(): MyMessage {
@@ -44,8 +44,7 @@ export const MyMessage = {
       writer.uint32(34).string(message.quux);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -122,8 +121,7 @@ function createBaseRequestType(): RequestType {
 export const RequestType = {
   encode(message: RequestType, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -172,8 +170,7 @@ function createBaseResponseType(): ResponseType {
 export const ResponseType = {
   encode(message: ResponseType, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
diff --git a/integration/unknown-fields/something/something.ts b/integration/unknown-fields/something/something.ts
index 8e083f96..48b9ebde 100644
--- a/integration/unknown-fields/something/something.ts
+++ b/integration/unknown-fields/something/something.ts
@@ -6,7 +6,7 @@ export const protobufPackage = "something";
 export interface Something {
   hello: string;
   foo: number[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 function createBaseSomething(): Something {
@@ -24,8 +24,7 @@ export const Something = {
     }
     writer.ldelim();
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
diff --git a/integration/use-date-false/google/protobuf/timestamp.ts b/integration/use-date-false/google/protobuf/timestamp.ts
index e4b5e083..dc3946fe 100644
--- a/integration/use-date-false/google/protobuf/timestamp.ts
+++ b/integration/use-date-false/google/protobuf/timestamp.ts
@@ -182,10 +182,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/use-date-string/google/protobuf/timestamp.ts b/integration/use-date-string/google/protobuf/timestamp.ts
index e4b5e083..dc3946fe 100644
--- a/integration/use-date-string/google/protobuf/timestamp.ts
+++ b/integration/use-date-string/google/protobuf/timestamp.ts
@@ -182,10 +182,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/use-date-true/google/protobuf/timestamp.ts b/integration/use-date-true/google/protobuf/timestamp.ts
index e4b5e083..dc3946fe 100644
--- a/integration/use-date-true/google/protobuf/timestamp.ts
+++ b/integration/use-date-true/google/protobuf/timestamp.ts
@@ -182,10 +182,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/use-json-wire-format/google/protobuf/duration.ts b/integration/use-json-wire-format/google/protobuf/duration.ts
index 2ec402df..a5338b56 100644
--- a/integration/use-json-wire-format/google/protobuf/duration.ts
+++ b/integration/use-json-wire-format/google/protobuf/duration.ts
@@ -68,7 +68,9 @@ export interface Duration {
    * to +315,576,000,000 inclusive. Note: these bounds are computed from:
    * 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years
    */
-  seconds?: number;
+  seconds?:
+    | number
+    | undefined;
   /**
    * Signed fractions of a second at nanosecond resolution of the span
    * of time. Durations less than one second are represented with a 0
@@ -77,5 +79,5 @@ export interface Duration {
    * of the same sign as the `seconds` field. Must be from -999,999,999
    * to +999,999,999 inclusive.
    */
-  nanos?: number;
+  nanos?: number | undefined;
 }
diff --git a/integration/use-json-wire-format/google/protobuf/field_mask.ts b/integration/use-json-wire-format/google/protobuf/field_mask.ts
index a97d9029..db1f33cb 100644
--- a/integration/use-json-wire-format/google/protobuf/field_mask.ts
+++ b/integration/use-json-wire-format/google/protobuf/field_mask.ts
@@ -204,5 +204,5 @@ export const protobufPackage = "google.protobuf";
  */
 export interface FieldMask {
   /** The set of field mask paths. */
-  paths?: string[];
+  paths?: string[] | undefined;
 }
diff --git a/integration/use-json-wire-format/google/protobuf/timestamp.ts b/integration/use-json-wire-format/google/protobuf/timestamp.ts
index 5f188796..cfd3d8c8 100644
--- a/integration/use-json-wire-format/google/protobuf/timestamp.ts
+++ b/integration/use-json-wire-format/google/protobuf/timestamp.ts
@@ -99,12 +99,14 @@ export interface Timestamp {
    * 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
    * 9999-12-31T23:59:59Z inclusive.
    */
-  seconds?: number;
+  seconds?:
+    | number
+    | undefined;
   /**
    * Non-negative fractions of a second at nanosecond resolution. Negative
    * second values with fractions must still have non-negative nanos values
    * that count forward in time. Must be from 0 to 999,999,999
    * inclusive.
    */
-  nanos?: number;
+  nanos?: number | undefined;
 }
diff --git a/integration/use-json-wire-format/use-json-wire-format.ts b/integration/use-json-wire-format/use-json-wire-format.ts
index 8f9d401e..15439502 100644
--- a/integration/use-json-wire-format/use-json-wire-format.ts
+++ b/integration/use-json-wire-format/use-json-wire-format.ts
@@ -3,8 +3,8 @@
 export const protobufPackage = "";
 
 export interface Todo {
-  id?: string;
-  timestamp?: string;
-  duration?: string;
-  updateMask?: string;
+  id?: string | undefined;
+  timestamp?: string | undefined;
+  duration?: string | undefined;
+  updateMask?: string | undefined;
 }
diff --git a/integration/use-map-type/google/protobuf/timestamp.ts b/integration/use-map-type/google/protobuf/timestamp.ts
index e4b5e083..dc3946fe 100644
--- a/integration/use-map-type/google/protobuf/timestamp.ts
+++ b/integration/use-map-type/google/protobuf/timestamp.ts
@@ -182,10 +182,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/use-map-type/use-map-type.ts b/integration/use-map-type/use-map-type.ts
index 0c494fe2..65f8fccc 100644
--- a/integration/use-map-type/use-map-type.ts
+++ b/integration/use-map-type/use-map-type.ts
@@ -687,10 +687,10 @@ export const Maps_MapOfTimestampsEntry = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/use-optionals-all/google/protobuf/timestamp.ts b/integration/use-optionals-all/google/protobuf/timestamp.ts
index 24a0bab6..dfd52f72 100644
--- a/integration/use-optionals-all/google/protobuf/timestamp.ts
+++ b/integration/use-optionals-all/google/protobuf/timestamp.ts
@@ -101,14 +101,16 @@ export interface Timestamp {
    * 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
    * 9999-12-31T23:59:59Z inclusive.
    */
-  seconds?: number;
+  seconds?:
+    | number
+    | undefined;
   /**
    * Non-negative fractions of a second at nanosecond resolution. Negative
    * second values with fractions must still have non-negative nanos values
    * that count forward in time. Must be from 0 to 999,999,999
    * inclusive.
    */
-  nanos?: number;
+  nanos?: number | undefined;
 }
 
 function createBaseTimestamp(): Timestamp {
@@ -182,10 +184,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/use-readonly-types/google/protobuf/struct.ts b/integration/use-readonly-types/google/protobuf/struct.ts
index 7b43ad34..10fbb618 100644
--- a/integration/use-readonly-types/google/protobuf/struct.ts
+++ b/integration/use-readonly-types/google/protobuf/struct.ts
@@ -72,7 +72,8 @@ export interface Value {
     | { readonly $case: "stringValue"; readonly stringValue: string }
     | { readonly $case: "boolValue"; readonly boolValue: boolean }
     | { readonly $case: "structValue"; readonly structValue: { readonly [key: string]: any } | undefined }
-    | { readonly $case: "listValue"; readonly listValue: ReadonlyArray<any> | undefined };
+    | { readonly $case: "listValue"; readonly listValue: ReadonlyArray<any> | undefined }
+    | undefined;
 }
 
 /**
diff --git a/integration/use-readonly-types/use-readonly-types.ts b/integration/use-readonly-types/use-readonly-types.ts
index a5f185be..e901e875 100644
--- a/integration/use-readonly-types/use-readonly-types.ts
+++ b/integration/use-readonly-types/use-readonly-types.ts
@@ -19,7 +19,7 @@ export interface Entity {
   readonly oneOfValue?: { readonly $case: "theStringValue"; readonly theStringValue: string } | {
     readonly $case: "theIntValue";
     readonly theIntValue: number;
-  };
+  } | undefined;
 }
 
 export interface SubEntity {
diff --git a/integration/value/google/protobuf/wrappers.ts b/integration/value/google/protobuf/wrappers.ts
index 99125575..356be214 100644
--- a/integration/value/google/protobuf/wrappers.ts
+++ b/integration/value/google/protobuf/wrappers.ts
@@ -599,10 +599,10 @@ export const BytesValue = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/vector-tile/vector_tile.ts b/integration/vector-tile/vector_tile.ts
index 93d2bd8d..ba885681 100644
--- a/integration/vector-tile/vector_tile.ts
+++ b/integration/vector-tile/vector_tile.ts
@@ -539,10 +539,10 @@ export const Tile_Layer = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/wrappers-regression/google/protobuf/timestamp.ts b/integration/wrappers-regression/google/protobuf/timestamp.ts
index e4b5e083..dc3946fe 100644
--- a/integration/wrappers-regression/google/protobuf/timestamp.ts
+++ b/integration/wrappers-regression/google/protobuf/timestamp.ts
@@ -182,10 +182,10 @@ export const Timestamp = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/wrappers-regression/google/protobuf/wrappers.ts b/integration/wrappers-regression/google/protobuf/wrappers.ts
index 99125575..356be214 100644
--- a/integration/wrappers-regression/google/protobuf/wrappers.ts
+++ b/integration/wrappers-regression/google/protobuf/wrappers.ts
@@ -599,10 +599,10 @@ export const BytesValue = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/jest.config.js b/jest.config.js
index 4de15779..ecce6b20 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,7 +1,16 @@
 /**  @type {import('@jest/types').Config.ProjectConfig} */
 module.exports = {
   transform: {
-    '^.+\\.ts$': 'ts-jest',
+    '^.+\\.ts$': [
+      'ts-jest',
+      {
+        // Note: We are limited to one single TS config even though we have
+        // vastly different TS configs for the sources and the tests. So we'll
+        // make do with a **very** loose config file. Any type checking should
+        // be done with `yarn tsc:check`.
+        tsconfig: 'tsconfig.json',
+      }
+    ],
   },
   resolver: 'jest-ts-webcompat-resolver',
   moduleFileExtensions: ['js', 'ts'],
diff --git a/package.json b/package.json
index b329ec7c..c126e866 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
     "proto2pbjs": "docker compose run --rm protoc pbjs.sh",
     "bin2ts": "docker compose run --rm protoc codegen.sh",
     "test": "yarn jest -c jest.config.js --maxWorkers=2",
+    "tsc:check": "./tsc-check.sh tsconfig.json integration/tsconfig.json integration/tsconfig.proto.json protos/tsconfig.json",
     "format": "prettier --write {src,tests}/**/*.ts integration/*.ts",
     "format:check": "prettier --list-different {src,tests}/**/*.ts",
     "setup:docker": "docker compose build",
@@ -42,8 +43,9 @@
     "@semantic-release/github": "^9.0.2",
     "@semantic-release/npm": "^10.0.3",
     "@semantic-release/release-notes-generator": "^11.0.2",
+    "@tsconfig/strictest": "^2.0.1",
     "@types/jest": "^29.5.2",
-    "@types/node": "^14.14.37",
+    "@types/node": "^16.18.38",
     "chokidar": "^3.5.3",
     "jest": "^29.5.0",
     "jest-ts-webcompat-resolver": "^1.0.0",
diff --git a/src/generate-grpc-web.ts b/src/generate-grpc-web.ts
index 57f8ba48..9b96fe40 100644
--- a/src/generate-grpc-web.ts
+++ b/src/generate-grpc-web.ts
@@ -258,17 +258,16 @@ function createPromiseUnaryMethod(ctx: Context): Code {
       ${useAbortSignal ? "abortSignal?: AbortSignal," : ""}
     ): Promise<any> {
       const request = { ..._request, ...methodDesc.requestType };
-      const maybeCombinedMetadata =
-        metadata && this.options.metadata
-          ? new ${BrowserHeaders}({ ...this.options?.metadata.headersMap, ...metadata?.headersMap })
-          : metadata || this.options.metadata;
+      const maybeCombinedMetadata = metadata && this.options.metadata
+        ? new ${BrowserHeaders}({ ...this.options?.metadata.headersMap, ...metadata?.headersMap })
+        : metadata ?? this.options.metadata;
       return new Promise((resolve, reject) => {
         ${useAbortSignal ? `const client =` : ""} ${grpc}.unary(methodDesc, {
           request,
           host: this.host,
-          metadata: maybeCombinedMetadata,
-          transport: this.options.transport,
-          debug: this.options.debug,
+          metadata: maybeCombinedMetadata ?? {},
+          ...(this.options.transport !== undefined ? {transport: this.options.transport} : {}),
+          debug: this.options.debug ?? false,
           onEnd: function (response) {
             if (response.status === grpc.Code.OK) {
               resolve(response.message!.toObject());
@@ -304,17 +303,16 @@ function createObservableUnaryMethod(ctx: Context): Code {
       ${useAbortSignal ? "abortSignal?: AbortSignal," : ""}
     ): ${observableType(ctx)}<any> {
       const request = { ..._request, ...methodDesc.requestType };
-      const maybeCombinedMetadata =
-        metadata && this.options.metadata
-          ? new ${BrowserHeaders}({ ...this.options?.metadata.headersMap, ...metadata?.headersMap })
-          : metadata || this.options.metadata;
+      const maybeCombinedMetadata = metadata && this.options.metadata
+        ? new ${BrowserHeaders}({ ...this.options?.metadata.headersMap, ...metadata?.headersMap })
+        : metadata ?? this.options.metadata;
       return new Observable(observer => {
         ${useAbortSignal ? `const client =` : ""} ${grpc}.unary(methodDesc, {
           request,
           host: this.host,
-          metadata: maybeCombinedMetadata,
-          transport: this.options.transport,
-          debug: this.options.debug,
+          metadata: maybeCombinedMetadata ?? {},
+          ...(this.options.transport !== undefined ? {transport: this.options.transport} : {}),
+          debug: this.options.debug ?? false,
           onEnd: (next) => {
             if (next.status !== 0) {
               const err = new ${ctx.utils.GrpcWebError}(next.statusMessage, next.status, next.trailers);
@@ -353,21 +351,21 @@ function createInvokeMethod(ctx: Context) {
       metadata: grpc.Metadata | undefined,
       ${useAbortSignal ? "abortSignal?: AbortSignal," : ""}
     ): ${observableType(ctx)}<any> {
-      const upStreamCodes = this.options.upStreamRetryCodes || [];
+      const upStreamCodes = this.options.upStreamRetryCodes ?? [];
       const DEFAULT_TIMEOUT_TIME: number = 3_000;
       const request = { ..._request, ...methodDesc.requestType };
-      const maybeCombinedMetadata =
-      metadata && this.options.metadata
+      const transport = this.options.streamingTransport ?? this.options.transport;
+      const maybeCombinedMetadata = metadata && this.options.metadata
         ? new ${BrowserHeaders}({ ...this.options?.metadata.headersMap, ...metadata?.headersMap })
-        : metadata || this.options.metadata;
+        : metadata ?? this.options.metadata;
       return new Observable(observer => {
         const upStream = (() => {
           const client = ${grpc}.invoke(methodDesc, {
             host: this.host,
             request,
-            transport: this.options.streamingTransport || this.options.transport,
-            metadata: maybeCombinedMetadata,
-            debug: this.options.debug,
+            ...(transport !== undefined ? {transport} : {}),
+            metadata: maybeCombinedMetadata ?? {},
+            debug: this.options.debug ?? false,
             onMessage: (next) => observer.next(next),
             onEnd: (code: ${grpc}.Code, message: string, trailers: ${grpc}.Metadata) => {
               if (code === 0) {
diff --git a/src/generate-services.ts b/src/generate-services.ts
index 3deb8bb7..2fe599fc 100644
--- a/src/generate-services.ts
+++ b/src/generate-services.ts
@@ -259,7 +259,7 @@ function generateBatchingRpcMethod(ctx: Context, batchMethod: BatchMethod): Code
     // If the return type is a map, lookup each key in the result
     lambda.push(code`
       return this.${methodDesc.formattedName}(ctx, request).then(res => {
-        return ${inputFieldName}.map(key => res.${outputFieldName}[key])
+        return ${inputFieldName}.map(key => res.${outputFieldName}[key] ?? ${ctx.utils.fail}())
       });
     `);
   } else {
diff --git a/src/main.ts b/src/main.ts
index d46340f6..a4860b12 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -401,7 +401,8 @@ export type Utils = ReturnType<typeof makeDeepPartial> &
   ReturnType<typeof makeComparisonUtils> &
   ReturnType<typeof makeNiceGrpcServerStreamingMethodResult> &
   ReturnType<typeof makeGrpcWebErrorClass> &
-  ReturnType<typeof makeExtensionClass>;
+  ReturnType<typeof makeExtensionClass> &
+  ReturnType<typeof makeAssertionUtils>;
 
 /** These are runtime utility methods used by the generated code. */
 export function makeUtils(options: Options): Utils {
@@ -417,6 +418,7 @@ export function makeUtils(options: Options): Utils {
     ...makeNiceGrpcServerStreamingMethodResult(),
     ...makeGrpcWebErrorClass(bytes),
     ...makeExtensionClass(options),
+    ...makeAssertionUtils(),
   };
 }
 
@@ -531,10 +533,10 @@ function makeByteUtils() {
   const globalThis = conditionalOutput(
     "tsProtoGlobalThis",
     code`
-      declare var self: any | undefined;
-      declare var window: any | undefined;
-      declare var global: any | undefined;
-      var tsProtoGlobalThis: any = (() => {
+      declare const self: any | undefined;
+      declare const window: any | undefined;
+      declare const global: any | undefined;
+      const tsProtoGlobalThis: any = (() => {
         if (typeof globalThis !== "undefined") return globalThis;
         if (typeof self !== "undefined") return self;
         if (typeof window !== "undefined") return window;
@@ -834,6 +836,19 @@ function makeExtensionClass(options: Options) {
   return { Extension };
 }
 
+function makeAssertionUtils() {
+  const fail = conditionalOutput(
+    "fail",
+    code`
+      function fail(message?: string): never {
+        throw new Error(message ?? "Failed");
+      }
+    `
+  );
+
+  return { fail };
+}
+
 // Create the interface with properties
 function generateInterfaceDeclaration(
   ctx: Context,
@@ -870,13 +885,16 @@ function generateInterfaceDeclaration(
     maybeAddComment(info, chunks, fieldDesc.options?.deprecated);
 
     const name = maybeSnakeToCamel(fieldDesc.name, options);
-    const type = toTypeName(ctx, messageDesc, fieldDesc);
-    const q = isOptionalProperty(fieldDesc, messageDesc.options, options) ? "?" : "";
-    chunks.push(code`${maybeReadonly(options)}${name}${q}: ${type}, `);
+    const isOptional = isOptionalProperty(fieldDesc, messageDesc.options, options);
+    const type = toTypeName(ctx, messageDesc, fieldDesc, isOptional);
+    if (isOptional && !type.toString().includes("undefined")) {
+      console.warn(name, type);
+    }
+    chunks.push(code`${maybeReadonly(options)}${name}${isOptional ? "?" : ""}: ${type}, `);
   });
 
   if (ctx.options.unknownFields) {
-    chunks.push(code`_unknownFields?: {[key: number]: Uint8Array[]},`);
+    chunks.push(code`_unknownFields?: {[key: number]: Uint8Array[]} | undefined,`);
   }
 
   chunks.push(code`}`);
@@ -902,7 +920,7 @@ function generateOneofProperty(
   );
 
   const name = maybeSnakeToCamel(messageDesc.oneofDecl[oneofIndex].name, options);
-  return code`${mbReadonly}${name}?: ${unionType},`;
+  return code`${mbReadonly}${name}?: ${unionType} | undefined,`;
 
   /*
   // Ideally we'd put the comments for each oneof field next to the anonymous
@@ -1428,8 +1446,7 @@ function generateEncode(ctx: Context, fullName: string, messageDesc: DescriptorP
 
   if (options.unknownFields) {
     chunks.push(code`if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -1639,7 +1656,7 @@ function generateExtension(ctx: Context, message: DescriptorProto | undefined, e
 
       // start loop over all buffers
       chunks.push(code`
-        for (var buffer of input) {
+        for (const buffer of input) {
           const reader = ${Reader}.create(buffer);
       `);
 
@@ -1669,7 +1686,7 @@ function generateExtension(ctx: Context, message: DescriptorProto | undefined, e
     } else {
       // pick the last entry, since it overrides all previous entries if not repeated
       chunks.push(code`
-          const reader = ${Reader}.create(input[input.length -1]);
+          const reader = ${Reader}.create(input[input.length -1] ?? ${ctx.utils.fail}());
           return ${readSnippet};
         },
       `);
diff --git a/src/types.ts b/src/types.ts
index bbee5701..1a074c06 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -561,16 +561,9 @@ export function messageToTypeName(
   // them to basic built-in types, we union the type with undefined to
   // indicate the value is optional. Exceptions:
   // - If the field is repeated, values cannot be undefined.
-  // - If useOptionals='messages' or useOptionals='all', all non-scalar types
-  //   are already optional properties, so there's no need for that union.
   let valueType = valueTypeName(ctx, protoType);
   if (!typeOptions.keepValueType && valueType) {
-    if (
-      !!typeOptions.repeated ||
-      options.useOptionals === true ||
-      options.useOptionals === "messages" ||
-      options.useOptionals === "all"
-    ) {
+    if (typeOptions.repeated ?? false) {
       return valueType;
     }
     return code`${valueType} | undefined`;
@@ -605,27 +598,39 @@ export function getEnumMethod(ctx: Context, enumProtoType: string, methodSuffix:
 }
 
 /** Return the TypeName for any field (primitive/message/etc.) as exposed in the interface. */
-export function toTypeName(ctx: Context, messageDesc: DescriptorProto | undefined, field: FieldDescriptorProto): Code {
+export function toTypeName(
+  ctx: Context,
+  messageDesc: DescriptorProto | undefined,
+  field: FieldDescriptorProto,
+  ensureOptional = false
+): Code {
+  function finalize(type: Code, isOptional: boolean) {
+    if (isOptional) {
+      return code`${type} | undefined`;
+    }
+    return type;
+  }
+
   let type = basicTypeName(ctx, field, { keepValueType: false });
   if (isRepeated(field)) {
     const mapType = messageDesc ? detectMapType(ctx, messageDesc, field) : false;
     if (mapType) {
       const { keyType, valueType } = mapType;
       if (ctx.options.useMapType) {
-        return code`Map<${keyType}, ${valueType}>`;
+        return finalize(code`Map<${keyType}, ${valueType}>`, ensureOptional);
       }
-      return code`{ [key: ${keyType} ]: ${valueType} }`;
+      return finalize(code`{ [key: ${keyType} ]: ${valueType} }`, ensureOptional);
     }
     if (ctx.options.useReadonlyTypes) {
-      return code`readonly ${type}[]`;
+      return finalize(code`readonly ${type}[]`, ensureOptional);
     }
-    return code`${type}[]`;
+    return finalize(code`${type}[]`, ensureOptional);
   }
 
   if (isValueType(ctx, field)) {
     // google.protobuf.*Value types are already unioned with `undefined`
     // in messageToTypeName, so no need to consider them for that here.
-    return type;
+    return finalize(type, false);
   }
 
   // By default (useOptionals='none', oneof=properties), non-scalar fields
@@ -640,17 +645,15 @@ export function toTypeName(ctx: Context, messageDesc: DescriptorProto | undefine
   // clause, spelling each option out inside a large type union. No need for
   // union with `undefined` here, either.
   const { options } = ctx;
-  if (
+  return finalize(
+    type,
     (!isWithinOneOf(field) &&
       isMessage(field) &&
       (options.useOptionals === false || options.useOptionals === "none")) ||
-    (isWithinOneOf(field) && options.oneof === OneofOption.PROPERTIES) ||
-    (isWithinOneOf(field) && field.proto3Optional)
-  ) {
-    return code`${type} | undefined`;
-  }
-
-  return type;
+      (isWithinOneOf(field) && options.oneof === OneofOption.PROPERTIES) ||
+      (isWithinOneOf(field) && field.proto3Optional) ||
+      ensureOptional
+  );
 }
 
 export function detectMapType(
diff --git a/tsc-check.sh b/tsc-check.sh
new file mode 100755
index 00000000..b6d4ad12
--- /dev/null
+++ b/tsc-check.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+for path in "$@"; do
+  echo Checking ${path}
+  yarn tsc --noEmit -p ${path}
+done
diff --git a/tsconfig.json b/tsconfig.json
index c74fef3c..bdd454a1 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -8,5 +8,5 @@
     "skipLibCheck": true,
     "experimentalDecorators": true
   },
-  "include": ["src"]
+  "include": ["src", "integration/codegen.ts", "tests"],
 }
diff --git a/yarn.lock b/yarn.lock
index 78014992..bcea3621 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1868,6 +1868,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@tsconfig/strictest@npm:^2.0.1":
+  version: 2.0.1
+  resolution: "@tsconfig/strictest@npm:2.0.1"
+  checksum: 3a9b913e06c20fee0a002fc22a925370525dd917853dee2e0082de763b10a61a69d72d60d02e243629da94b3e0b98843469a3b063c0ab2a5d6716183867dd114
+  languageName: node
+  linkType: hard
+
 "@tufjs/canonical-json@npm:1.0.0":
   version: 1.0.0
   resolution: "@tufjs/canonical-json@npm:1.0.0"
@@ -1991,7 +1998,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:^14.14.37":
+"@types/node@npm:*, @types/node@npm:>=12.12.47":
   version: 14.14.37
   resolution: "@types/node@npm:14.14.37"
   checksum: 647e671ac3815f428a07ae9353ebab93c5335d4d0a461ca837a79eebf55c04f28bee80f0c43881cdd8696f595feb112bc8eb74f6124125e02f79a7677374c187
@@ -2012,6 +2019,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/node@npm:^16.18.38":
+  version: 16.18.38
+  resolution: "@types/node@npm:16.18.38"
+  checksum: a3baa141e49ce94486f083eea1240cf38479a73ba663e1bf3f52f85b466125821b6e3ea85ded38fde3901530aca4601291395a50eefcea533a4f3b45171bda28
+  languageName: node
+  linkType: hard
+
 "@types/normalize-package-data@npm:^2.4.0":
   version: 2.4.0
   resolution: "@types/normalize-package-data@npm:2.4.0"
@@ -7992,8 +8006,9 @@ __metadata:
     "@semantic-release/github": ^9.0.2
     "@semantic-release/npm": ^10.0.3
     "@semantic-release/release-notes-generator": ^11.0.2
+    "@tsconfig/strictest": ^2.0.1
     "@types/jest": ^29.5.2
-    "@types/node": ^14.14.37
+    "@types/node": ^16.18.38
     "@types/object-hash": ^3.0.2
     case-anything: ^2.1.10
     chokidar: ^3.5.3


Test patch:
diff --git a/integration/extensions/test.ts b/integration/extensions/test.ts
index c840b079..03cf234d 100644
--- a/integration/extensions/test.ts
+++ b/integration/extensions/test.ts
@@ -44,19 +44,19 @@ export function enumToJSON(object: Enum): string {
 }
 
 export interface Extendable {
-  field?: string;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  field?: string | undefined;
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface Nested {
-  field?: string;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  field?: string | undefined;
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface Group {
-  name?: string;
-  value?: string;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  name?: string | undefined;
+  value?: string | undefined;
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 function createBaseExtendable(): Extendable {
@@ -69,8 +69,7 @@ export const Extendable = {
       writer.uint32(10).string(message.field);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -216,7 +215,7 @@ export const Nested = {
     },
     decode: (tag: number, input: Uint8Array[]): Nested[] => {
       const values: Nested[] = [];
-      for (var buffer of input) {
+      for (const buffer of input) {
         const reader = _m0.Reader.create(buffer);
         values.push(Nested.decode(reader, reader.uint32()));
       }
@@ -230,8 +229,7 @@ export const Nested = {
       writer.uint32(10).string(message.field);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -317,8 +315,7 @@ export const Group = {
       writer.uint32(18).string(message.value);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -422,7 +419,7 @@ export const packed: Extension<number[]> = {
   },
   decode: (tag: number, input: Uint8Array[]): number[] => {
     const values: number[] = [];
-    for (var buffer of input) {
+    for (const buffer of input) {
       const reader = _m0.Reader.create(buffer);
       if (tag == 42) {
         const end2 = reader.uint32() + reader.pos;
@@ -457,7 +454,7 @@ export const repeated: Extension<number[]> = {
   },
   decode: (tag: number, input: Uint8Array[]): number[] => {
     const values: number[] = [];
-    for (var buffer of input) {
+    for (const buffer of input) {
       const reader = _m0.Reader.create(buffer);
       if (tag == 50) {
         const end2 = reader.uint32() + reader.pos;
@@ -488,7 +485,7 @@ export const bytes: Extension<Uint8Array> = {
     return encoded;
   },
   decode: (tag: number, input: Uint8Array[]): Uint8Array => {
-    const reader = _m0.Reader.create(input[input.length - 1]);
+    const reader = _m0.Reader.create(input[input.length - 1] ?? fail());
     return reader.bytes();
   },
 };
@@ -508,7 +505,7 @@ export const string: Extension<string> = {
     return encoded;
   },
   decode: (tag: number, input: Uint8Array[]): string => {
-    const reader = _m0.Reader.create(input[input.length - 1]);
+    const reader = _m0.Reader.create(input[input.length - 1] ?? fail());
     return reader.string();
   },
 };
@@ -528,7 +525,7 @@ export const long: Extension<Long> = {
     return encoded;
   },
   decode: (tag: number, input: Uint8Array[]): Long => {
-    const reader = _m0.Reader.create(input[input.length - 1]);
+    const reader = _m0.Reader.create(input[input.length - 1] ?? fail());
     return reader.int64() as Long;
   },
 };
@@ -548,7 +545,7 @@ export const fixed: Extension<Long> = {
     return encoded;
   },
   decode: (tag: number, input: Uint8Array[]): Long => {
-    const reader = _m0.Reader.create(input[input.length - 1]);
+    const reader = _m0.Reader.create(input[input.length - 1] ?? fail());
     return reader.fixed64() as Long;
   },
 };
@@ -568,7 +565,7 @@ export const enumField: Extension<Enum> = {
     return encoded;
   },
   decode: (tag: number, input: Uint8Array[]): Enum => {
-    const reader = _m0.Reader.create(input[input.length - 1]);
+    const reader = _m0.Reader.create(input[input.length - 1] ?? fail());
     return reader.int32() as any;
   },
 };
@@ -586,7 +583,7 @@ export const group: Extension<Group> = {
     return encoded;
   },
   decode: (tag: number, input: Uint8Array[]): Group => {
-    const reader = _m0.Reader.create(input[input.length - 1]);
+    const reader = _m0.Reader.create(input[input.length - 1] ?? fail());
     return Group.decode(reader);
   },
 };
@@ -623,3 +620,7 @@ export interface Extension<T> {
   repeated: boolean;
   packed: boolean;
 }
+
+function fail(message?: string): never {
+  throw new Error(message ?? "Failed");
+}
diff --git a/integration/from-partial-no-initialize/test.ts b/integration/from-partial-no-initialize/test.ts
index 9fd609d8..6407b23d 100644
--- a/integration/from-partial-no-initialize/test.ts
+++ b/integration/from-partial-no-initialize/test.ts
@@ -4,17 +4,17 @@ import * as _m0 from "protobufjs/minimal";
 export const protobufPackage = "";
 
 export interface TPartialMessage {
-  field?: string;
+  field?: string | undefined;
 }
 
 export interface TPartial {
-  number?: number;
-  string?: string;
-  map?: { [key: string]: string };
-  message?: TPartialMessage;
-  repeatedMessage?: TPartialMessage[];
-  repeatedString?: string[];
-  repeatedNumber?: number[];
+  number?: number | undefined;
+  string?: string | undefined;
+  map?: { [key: string]: string } | undefined;
+  message?: TPartialMessage | undefined;
+  repeatedMessage?: TPartialMessage[] | undefined;
+  repeatedString?: string[] | undefined;
+  repeatedNumber?: number[] | undefined;
 }
 
 export interface TPartial_MapEntry {
diff --git a/integration/groups/test.ts b/integration/groups/test.ts
index 1950a5ec..118998c2 100644
--- a/integration/groups/test.ts
+++ b/integration/groups/test.ts
@@ -4,51 +4,51 @@ import * as _m0 from "protobufjs/minimal";
 export const protobufPackage = "";
 
 export interface GroupsOptionalTest {
-  int1?: number;
-  group?: GroupsOptionalTest_Group;
-  int3?: number;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  int1?: number | undefined;
+  group?: GroupsOptionalTest_Group | undefined;
+  int3?: number | undefined;
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface GroupsOptionalTest_Group {
-  key?: string;
-  value?: string;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  key?: string | undefined;
+  value?: string | undefined;
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface GroupsRepeatedTest {
-  int1?: number[];
-  group?: GroupsRepeatedTest_Group[];
-  int3?: number[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  int1?: number[] | undefined;
+  group?: GroupsRepeatedTest_Group[] | undefined;
+  int3?: number[] | undefined;
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface GroupsRepeatedTest_Group {
-  key?: string[];
-  value?: string[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  key?: string[] | undefined;
+  value?: string[] | undefined;
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface GroupsNestedTest {
-  int1?: number[];
-  group?: GroupsNestedTest_Group[];
-  int3?: number[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  int1?: number[] | undefined;
+  group?: GroupsNestedTest_Group[] | undefined;
+  int3?: number[] | undefined;
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface GroupsNestedTest_Group {
-  nested?: GroupsNestedTest_Group_Nested[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  nested?: GroupsNestedTest_Group_Nested[] | undefined;
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface GroupsNestedTest_Group_Nested {
-  nested2?: GroupsNestedTest_Group_Nested_Nested2[];
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  nested2?: GroupsNestedTest_Group_Nested_Nested2[] | undefined;
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface GroupsNestedTest_Group_Nested_Nested2 {
-  string1?: string;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  string1?: string | undefined;
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 function createBaseGroupsOptionalTest(): GroupsOptionalTest {
@@ -67,8 +67,7 @@ export const GroupsOptionalTest = {
       writer.uint32(24).int32(message.int3);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -179,8 +178,7 @@ export const GroupsOptionalTest_Group = {
       writer.uint32(18).string(message.value);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -291,8 +289,7 @@ export const GroupsRepeatedTest = {
       writer.ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -453,8 +450,7 @@ export const GroupsRepeatedTest_Group = {
       }
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -579,8 +575,7 @@ export const GroupsNestedTest = {
       writer.ldelim();
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -736,8 +731,7 @@ export const GroupsNestedTest_Group = {
       }
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -833,8 +827,7 @@ export const GroupsNestedTest_Group_Nested = {
       }
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -930,8 +923,7 @@ export const GroupsNestedTest_Group_Nested_Nested2 = {
       writer.uint32(10).string(message.string1);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
diff --git a/integration/map-bigint-optional/test.ts b/integration/map-bigint-optional/test.ts
index 5da264d4..6c91a8b5 100644
--- a/integration/map-bigint-optional/test.ts
+++ b/integration/map-bigint-optional/test.ts
@@ -5,14 +5,14 @@ import * as _m0 from "protobufjs/minimal";
 export const protobufPackage = "";
 
 export interface MapBigInt {
-  map?: Map<bigint, bigint>;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  map?: Map<bigint, bigint> | undefined;
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface MapBigInt_MapEntry {
   key: bigint;
   value: bigint;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 function createBaseMapBigInt(): MapBigInt {
@@ -25,8 +25,7 @@ export const MapBigInt = {
       MapBigInt_MapEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).ldelim();
     });
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -138,8 +137,7 @@ export const MapBigInt_MapEntry = {
       writer.uint32(16).int64(message.value.toString());
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
diff --git a/integration/map-long-optional/test.ts b/integration/map-long-optional/test.ts
index a7c0242b..436cfd39 100644
--- a/integration/map-long-optional/test.ts
+++ b/integration/map-long-optional/test.ts
@@ -5,14 +5,14 @@ import * as _m0 from "protobufjs/minimal";
 export const protobufPackage = "";
 
 export interface MapBigInt {
-  map?: Map<Long, Long>;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  map?: Map<Long, Long> | undefined;
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface MapBigInt_MapEntry {
   key: Long;
   value: Long;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 function createBaseMapBigInt(): MapBigInt {
@@ -25,8 +25,7 @@ export const MapBigInt = {
       MapBigInt_MapEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).ldelim();
     });
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -138,8 +137,7 @@ export const MapBigInt_MapEntry = {
       writer.uint32(16).int64(message.value);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -224,10 +222,10 @@ export const MapBigInt_MapEntry = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/map-longstring-optional/test.ts b/integration/map-longstring-optional/test.ts
index e4b0da81..1dfad9e5 100644
--- a/integration/map-longstring-optional/test.ts
+++ b/integration/map-longstring-optional/test.ts
@@ -5,14 +5,14 @@ import * as _m0 from "protobufjs/minimal";
 export const protobufPackage = "";
 
 export interface MapBigInt {
-  map?: Map<string, string>;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  map?: Map<string, string> | undefined;
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 export interface MapBigInt_MapEntry {
   key: string;
   value: string;
-  _unknownFields?: { [key: number]: Uint8Array[] };
+  _unknownFields?: { [key: number]: Uint8Array[] } | undefined;
 }
 
 function createBaseMapBigInt(): MapBigInt {
@@ -25,8 +25,7 @@ export const MapBigInt = {
       MapBigInt_MapEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).ldelim();
     });
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
@@ -138,8 +137,7 @@ export const MapBigInt_MapEntry = {
       writer.uint32(16).int64(message.value);
     }
     if (message._unknownFields !== undefined) {
-      for (const key in message._unknownFields) {
-        const values = message._unknownFields[key];
+      for (const [key, values] of Object.entries(message._unknownFields)) {
         const tag = parseInt(key, 10);
         for (const value of values) {
           writer.uint32(tag);
diff --git a/integration/use-optionals-all/test.ts b/integration/use-optionals-all/test.ts
index 002e6529..24afe279 100644
--- a/integration/use-optionals-all/test.ts
+++ b/integration/use-optionals-all/test.ts
@@ -45,20 +45,20 @@ export function stateEnumToJSON(object: StateEnum): string {
 }
 
 export interface OptionalsTest {
-  id?: number;
-  child?: Child;
-  state?: StateEnum;
-  long?: number;
-  truth?: boolean;
-  description?: string;
-  data?: Uint8Array;
-  repId?: number[];
-  repChild?: Child[];
-  repState?: StateEnum[];
-  repLong?: number[];
-  repTruth?: boolean[];
-  repDescription?: string[];
-  repData?: Uint8Array[];
+  id?: number | undefined;
+  child?: Child | undefined;
+  state?: StateEnum | undefined;
+  long?: number | undefined;
+  truth?: boolean | undefined;
+  description?: string | undefined;
+  data?: Uint8Array | undefined;
+  repId?: number[] | undefined;
+  repChild?: Child[] | undefined;
+  repState?: StateEnum[] | undefined;
+  repLong?: number[] | undefined;
+  repTruth?: boolean[] | undefined;
+  repDescription?: string[] | undefined;
+  repData?: Uint8Array[] | undefined;
   optId?: number | undefined;
   optChild?: Child | undefined;
   optState?: StateEnum | undefined;
@@ -66,8 +66,8 @@ export interface OptionalsTest {
   optTruth?: boolean | undefined;
   optDescription?: string | undefined;
   optData?: Uint8Array | undefined;
-  translations?: { [key: string]: string };
-  timestamp?: Date;
+  translations?: { [key: string]: string } | undefined;
+  timestamp?: Date | undefined;
 }
 
 export interface OptionalsTest_TranslationsEntry {
@@ -676,10 +676,10 @@ export const Child = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/integration/use-optionals-no-undefined/test.ts b/integration/use-optionals-no-undefined/test.ts
index 78a69902..7125bf0a 100644
--- a/integration/use-optionals-no-undefined/test.ts
+++ b/integration/use-optionals-no-undefined/test.ts
@@ -44,20 +44,20 @@ export function stateEnumToJSON(object: StateEnum): string {
 }
 
 export interface OptionalsTest {
-  id?: number;
-  child?: Child;
-  state?: StateEnum;
-  long?: number;
-  truth?: boolean;
-  description?: string;
-  data?: Uint8Array;
-  repId?: number[];
-  repChild?: Child[];
-  repState?: StateEnum[];
-  repLong?: number[];
-  repTruth?: boolean[];
-  repDescription?: string[];
-  repData?: Uint8Array[];
+  id?: number | undefined;
+  child?: Child | undefined;
+  state?: StateEnum | undefined;
+  long?: number | undefined;
+  truth?: boolean | undefined;
+  description?: string | undefined;
+  data?: Uint8Array | undefined;
+  repId?: number[] | undefined;
+  repChild?: Child[] | undefined;
+  repState?: StateEnum[] | undefined;
+  repLong?: number[] | undefined;
+  repTruth?: boolean[] | undefined;
+  repDescription?: string[] | undefined;
+  repData?: Uint8Array[] | undefined;
   optId?: number | undefined;
   optChild?: Child | undefined;
   optState?: StateEnum | undefined;
@@ -65,7 +65,7 @@ export interface OptionalsTest {
   optTruth?: boolean | undefined;
   optDescription?: string | undefined;
   optData?: Uint8Array | undefined;
-  translations?: { [key: string]: string };
+  translations?: { [key: string]: string } | undefined;
 }
 
 export interface OptionalsTest_TranslationsEntry {
@@ -674,10 +674,10 @@ export const Child = {
   },
 };
 
-declare var self: any | undefined;
-declare var window: any | undefined;
-declare var global: any | undefined;
-var tsProtoGlobalThis: any = (() => {
+declare const self: any | undefined;
+declare const window: any | undefined;
+declare const global: any | undefined;
+const tsProtoGlobalThis: any = (() => {
   if (typeof globalThis !== "undefined") {
     return globalThis;
   }
diff --git a/tests/types-test.ts b/tests/types-test.ts
index 9d37b218..cea14e41 100644
--- a/tests/types-test.ts
+++ b/tests/types-test.ts
@@ -45,14 +45,14 @@ describe("types", () => {
         typeMap: new Map(),
         protoType: ".google.protobuf.StringValue",
         options: { ...defaultOptions(), useOptionals: true },
-        expected: code`string`,
+        expected: code`string | undefined`,
       },
       {
         descr: 'value types (useOptionals="all")',
         typeMap: new Map(),
         protoType: ".google.protobuf.StringValue",
         options: { ...defaultOptions(), useOptionals: "all" },
-        expected: code`string`,
+        expected: code`string | undefined`,
       },
     ];
     testCases.forEach((t) =>