diff --git a/lib/actions/activity-actions.js b/lib/actions/activity-actions.js
--- a/lib/actions/activity-actions.js
+++ b/lib/actions/activity-actions.js
@@ -25,7 +25,7 @@
   ): ((input: UpdateActivityInput) => Promise<ActivityUpdateSuccessPayload>) =>
   async input => {
     const { activityUpdates } = input;
-    const requests = {};
+    const requests: { [string]: { +updates: ActivityUpdate[] } } = {};
     for (const update of activityUpdates) {
       const keyserverID = extractKeyserverIDFromID(update.threadID);
       if (!requests[keyserverID]) {
@@ -36,7 +36,7 @@
 
     const responses = await callKeyserverEndpoint('update_activity', requests);
 
-    let unfocusedToUnread = [];
+    let unfocusedToUnread: $ReadOnlyArray<string> = [];
     for (const keyserverID in responses) {
       unfocusedToUnread = unfocusedToUnread.concat(
         responses[keyserverID].unfocusedToUnread,
diff --git a/lib/actions/device-actions.js b/lib/actions/device-actions.js
--- a/lib/actions/device-actions.js
+++ b/lib/actions/device-actions.js
@@ -1,6 +1,9 @@
 // @flow
 
-import type { GetVersionActionPayload } from '../types/device-types';
+import type {
+  GetVersionActionPayload,
+  DeviceTokenUpdateRequest,
+} from '../types/device-types';
 import { getConfig } from '../utils/config.js';
 import { useKeyserverCall } from '../utils/keyserver-call.js';
 import type {
@@ -25,7 +28,7 @@
     callKeyserverEndpoint: CallKeyserverEndpoint,
   ): ((input: DeviceTokens) => Promise<SetDeviceTokenActionPayload>) =>
   async input => {
-    const requests = {};
+    const requests: { [string]: DeviceTokenUpdateRequest } = {};
     for (const keyserverID in input) {
       requests[keyserverID] = {
         deviceToken: input[keyserverID],
@@ -48,8 +51,8 @@
     allKeyserverIDs: $ReadOnlyArray<string>,
   ): ((input: ?string) => Promise<SetDeviceTokenActionPayload>) =>
   async input => {
-    const requests = {};
-    const deviceTokens = {};
+    const requests: { [string]: DeviceTokenUpdateRequest } = {};
+    const deviceTokens: { [string]: ?string } = {};
     for (const keyserverID of allKeyserverIDs) {
       requests[keyserverID] = {
         deviceToken: input,
@@ -84,7 +87,7 @@
     allKeyserverIDs: $ReadOnlyArray<string>,
   ): (() => Promise<GetVersionActionPayload>) =>
   async () => {
-    const requests = {};
+    const requests: { [string]: {} } = {};
     for (const keyserverID of allKeyserverIDs) {
       requests[keyserverID] = {};
     }
diff --git a/lib/actions/entry-actions.js b/lib/actions/entry-actions.js
--- a/lib/actions/entry-actions.js
+++ b/lib/actions/entry-actions.js
@@ -39,13 +39,13 @@
       calendarQuery,
       allKeyserverIDs,
     );
-    const requests = {};
+    const requests: { [string]: CalendarQuery } = {};
     for (const keyserverID of allKeyserverIDs) {
       requests[keyserverID] = calendarQueries[keyserverID];
     }
 
     const responses = await callKeyserverEndpoint('fetch_entries', requests);
-    let rawEntryInfos = [];
+    let rawEntryInfos: $ReadOnlyArray<RawEntryInfo> = [];
     for (const keyserverID in responses) {
       rawEntryInfos = rawEntryInfos.concat(
         responses[keyserverID].rawEntryInfos,
@@ -87,7 +87,7 @@
       allKeyserverIDs,
     );
 
-    const requests = {};
+    const requests: { [string]: CalendarQuery } = {};
     for (const keyserverID of allKeyserverIDs) {
       requests[keyserverID] = calendarQueries[keyserverID];
     }
@@ -96,8 +96,8 @@
       'update_calendar_query',
       requests,
     );
-    let rawEntryInfos = [];
-    let deletedEntryIDs = [];
+    let rawEntryInfos: $ReadOnlyArray<RawEntryInfo> = [];
+    let deletedEntryIDs: $ReadOnlyArray<string> = [];
     for (const keyserverID in responses) {
       rawEntryInfos = rawEntryInfos.concat(
         responses[keyserverID].rawEntryInfos,
diff --git a/lib/actions/link-actions.js b/lib/actions/link-actions.js
--- a/lib/actions/link-actions.js
+++ b/lib/actions/link-actions.js
@@ -49,7 +49,7 @@
     allKeyserverIDs: $ReadOnlyArray<string>,
   ): (() => Promise<FetchInviteLinksResponse>) =>
   async () => {
-    const requests = {};
+    const requests: { [string]: void } = {};
     for (const keyserverID of allKeyserverIDs) {
       requests[keyserverID] = undefined;
     }
@@ -57,7 +57,7 @@
       'fetch_primary_invite_links',
       requests,
     );
-    let links = [];
+    let links: $ReadOnlyArray<InviteLink> = [];
     for (const keyserverID in responses) {
       links = links.concat(responses[keyserverID].links);
     }
diff --git a/lib/actions/message-actions.js b/lib/actions/message-actions.js
--- a/lib/actions/message-actions.js
+++ b/lib/actions/message-actions.js
@@ -13,6 +13,9 @@
   FetchPinnedMessagesResult,
   SearchMessagesRequest,
   SearchMessagesResponse,
+  FetchMessageInfosRequest,
+  RawMessageInfo,
+  MessageTruncationStatuses,
 } from '../types/message-types.js';
 import type { MediaMessageServerDBContent } from '../types/messages/media.js';
 import type {
@@ -121,7 +124,7 @@
   async threadIDs => {
     const sortedThreadIDs = sortThreadIDsPerKeyserver(threadIDs);
 
-    const requests = {};
+    const requests: { [string]: FetchMessageInfosRequest } = {};
     for (const keyserverID in sortedThreadIDs) {
       const cursors = Object.fromEntries(
         sortedThreadIDs[keyserverID].map(threadID => [threadID, null]),
@@ -133,8 +136,8 @@
     }
 
     const responses = await callKeyserverEndpoint('fetch_messages', requests);
-    let rawMessageInfos = [];
-    let truncationStatuses = {};
+    let rawMessageInfos: $ReadOnlyArray<RawMessageInfo> = [];
+    let truncationStatuses: MessageTruncationStatuses = {};
     for (const keyserverID in responses) {
       rawMessageInfos = rawMessageInfos.concat(
         responses[keyserverID].rawMessageInfos,
diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js
--- a/lib/actions/user-actions.js
+++ b/lib/actions/user-actions.js
@@ -11,11 +11,17 @@
   PolicyAcknowledgmentRequest,
   ClaimUsernameResponse,
   LogInResponse,
+  LogInRequest,
 } from '../types/account-types.js';
 import type {
   UpdateUserAvatarRequest,
   UpdateUserAvatarResponse,
 } from '../types/avatar-types.js';
+import type { RawEntryInfo, CalendarQuery } from '../types/entry-types.js';
+import type {
+  RawMessageInfo,
+  MessageTruncationStatuses,
+} from '../types/message-types.js';
 import type {
   GetSessionPublicKeysArgs,
   GetOlmSessionInitializationDataResponse,
@@ -68,7 +74,7 @@
     allKeyserverIDs: $ReadOnlyArray<string>,
   ): ((input: PreRequestUserState) => Promise<LogOutResult>) =>
   async preRequestUserState => {
-    const requests = {};
+    const requests: { [string]: {} } = {};
     for (const keyserverID of allKeyserverIDs) {
       requests[keyserverID] = {};
     }
@@ -128,7 +134,7 @@
     allKeyserverIDs: $ReadOnlyArray<string>,
   ): ((input: PreRequestUserState) => Promise<LogOutResult>) =>
   async preRequestUserState => {
-    const requests = {};
+    const requests: { [string]: {} } = {};
     for (const keyserverID of allKeyserverIDs) {
       requests[keyserverID] = {};
     }
@@ -181,7 +187,9 @@
     };
   };
 
-function mergeUserInfos(...userInfoArrays: UserInfo[][]): UserInfo[] {
+function mergeUserInfos(
+  ...userInfoArrays: Array<$ReadOnlyArray<UserInfo>>
+): UserInfo[] {
   const merged: { [string]: UserInfo } = {};
   for (const userInfoArray of userInfoArrays) {
     for (const userInfo of userInfoArray) {
@@ -195,6 +203,17 @@
   return flattened;
 }
 
+type WritableGenericMessagesResult = {
+  messageInfos: RawMessageInfo[],
+  truncationStatus: MessageTruncationStatuses,
+  watchedIDsAtRequestTime: string[],
+  currentAsOf: { [keyserverID: string]: number },
+};
+type WritableCalendarResult = {
+  rawEntryInfos: RawEntryInfo[],
+  calendarQuery: CalendarQuery,
+};
+
 const logInActionTypes = Object.freeze({
   started: 'LOG_IN_STARTED',
   success: 'LOG_IN_SUCCESS',
@@ -224,7 +243,7 @@
       keyserverIDs,
     );
 
-    const requests = {};
+    const requests: { [string]: LogInRequest } = {};
     for (const keyserverID of keyserverIDs) {
       requests[keyserverID] = {
         ...restLogInInfo,
@@ -246,11 +265,11 @@
     const userInfosArrays = [];
 
     let threadInfos: RawThreadInfos = {};
-    const calendarResult = {
+    const calendarResult: WritableCalendarResult = {
       calendarQuery: logInInfo.calendarQuery,
       rawEntryInfos: [],
     };
-    const messagesResult = {
+    const messagesResult: WritableGenericMessagesResult = {
       messageInfos: [],
       truncationStatus: {},
       watchedIDsAtRequestTime: watchedIDs,
@@ -398,7 +417,7 @@
     allKeyserverIDs: $ReadOnlyArray<string>,
   ): ((input: UpdateUserSettingsRequest) => Promise<void>) =>
   async input => {
-    const requests = {};
+    const requests: { [string]: UpdateUserSettingsRequest } = {};
     for (const keyserverID of allKeyserverIDs) {
       requests[keyserverID] = input;
     }
diff --git a/lib/selectors/account-selectors.js b/lib/selectors/account-selectors.js
--- a/lib/selectors/account-selectors.js
+++ b/lib/selectors/account-selectors.js
@@ -9,7 +9,10 @@
   deviceTokensSelector,
 } from './keyserver-selectors.js';
 import { currentCalendarQuery } from './nav-selectors.js';
-import type { LogInExtraInfo } from '../types/account-types.js';
+import type {
+  LogInExtraInfo,
+  DeviceTokenUpdateRequest,
+} from '../types/account-types.js';
 import type { CalendarQuery } from '../types/entry-types.js';
 import type { KeyserverInfos } from '../types/keyserver-types.js';
 import type { AppState } from '../types/redux-types.js';
@@ -28,7 +31,7 @@
     deviceTokens: { +[keyserverID: string]: ?string },
     calendarQuery: (calendarActive: boolean) => CalendarQuery,
   ) => {
-    const deviceTokenUpdateRequest = {};
+    const deviceTokenUpdateRequest: { [string]: DeviceTokenUpdateRequest } = {};
 
     for (const keyserverID in deviceTokens) {
       if (deviceTokens[keyserverID]) {
diff --git a/lib/selectors/keyserver-selectors.js b/lib/selectors/keyserver-selectors.js
--- a/lib/selectors/keyserver-selectors.js
+++ b/lib/selectors/keyserver-selectors.js
@@ -124,7 +124,7 @@
 } = createSelector(
   (state: AppState) => state.keyserverStore.keyserverInfos,
   (infos: { +[key: string]: KeyserverInfo }) => {
-    const deviceTokens = {};
+    const deviceTokens: { [string]: ?string } = {};
     for (const keyserverID in infos) {
       deviceTokens[keyserverID] = infos[keyserverID].deviceToken;
     }
diff --git a/lib/types/account-types.js b/lib/types/account-types.js
--- a/lib/types/account-types.js
+++ b/lib/types/account-types.js
@@ -44,7 +44,7 @@
   +password: string,
 };
 
-type DeviceTokenUpdateRequest = {
+export type DeviceTokenUpdateRequest = {
   +deviceToken: string,
 };
 
diff --git a/lib/types/filter-types.js b/lib/types/filter-types.js
--- a/lib/types/filter-types.js
+++ b/lib/types/filter-types.js
@@ -18,7 +18,7 @@
   +type: 'threads',
   +threadIDs: $ReadOnlyArray<string>,
 };
-type NotDeletedFilter = { +type: 'not_deleted' };
+export type NotDeletedFilter = { +type: 'not_deleted' };
 export type CalendarFilter = NotDeletedFilter | CalendarThreadFilter;
 
 export const calendarFilterValidator: TUnion<CalendarFilter> = t.union([
diff --git a/lib/utils/action-utils.js b/lib/utils/action-utils.js
--- a/lib/utils/action-utils.js
+++ b/lib/utils/action-utils.js
@@ -24,6 +24,7 @@
 import type { PlatformDetails } from '../types/device-types.js';
 import type { Endpoint, SocketAPIHandler } from '../types/endpoints.js';
 import type { CalendarQuery } from '../types/entry-types.js';
+import type { NotDeletedFilter } from '../types/filter-types.js';
 import type { LoadingOptions, LoadingInfo } from '../types/loading-types.js';
 import type {
   ActionPayload,
@@ -519,7 +520,7 @@
 function sortThreadIDsPerKeyserver(threadIDs: $ReadOnlyArray<string>): {
   +[keyserverID: string]: $ReadOnlyArray<string>,
 } {
-  const results = {};
+  const results: { [string]: string[] } = {};
   for (const threadID of threadIDs) {
     const keyserverID = extractKeyserverIDFromID(threadID);
     invariant(keyserverID, 'keyserver data missing from thread id');
@@ -531,6 +532,18 @@
   return results;
 }
 
+type CalendarThreadFilterWithWritableThreadIDs = {
+  +type: 'threads',
+  +threadIDs: string[],
+};
+type CalendarFilterWithWritableThreadIDs =
+  | NotDeletedFilter
+  | CalendarThreadFilterWithWritableThreadIDs;
+type CalendarQueryWithWritableFilters = {
+  +startDate: string,
+  +endDate: string,
+  +filters: CalendarFilterWithWritableThreadIDs[],
+};
 function sortCalendarQueryPerKeyserver(
   calendarQuery: CalendarQuery,
   keyserverIDs: $ReadOnlyArray<string>,
@@ -538,7 +551,7 @@
   +[keyserverID: string]: CalendarQuery,
 } {
   const { startDate, endDate, filters } = calendarQuery;
-  const results = {};
+  const results: { [string]: CalendarQueryWithWritableFilters } = {};
 
   for (const keyserverID of keyserverIDs) {
     results[keyserverID] = {
@@ -562,6 +575,10 @@
         let threadFilter = results[keyserverID].filters.find(
           flt => flt.type === 'threads',
         );
+        invariant(
+          !threadFilter || threadFilter.type === 'threads',
+          'should only match CalendarThreadFilter',
+        );
         if (!threadFilter) {
           threadFilter = { type: 'threads', threadIDs: [] };
           results[keyserverID].filters.push(threadFilter);
diff --git a/lib/utils/keyserver-call.js b/lib/utils/keyserver-call.js
--- a/lib/utils/keyserver-call.js
+++ b/lib/utils/keyserver-call.js
@@ -2,7 +2,6 @@
 
 import _memoize from 'lodash/memoize.js';
 import * as React from 'react';
-import { useDispatch } from 'react-redux';
 import { createSelector } from 'reselect';
 
 import { bindCookieAndUtilsIntoCallServerEndpoint } from './action-utils.js';
@@ -12,7 +11,7 @@
   CallServerEndpointOptions,
 } from './call-server-endpoint.js';
 import { promiseAll } from './promises.js';
-import { useSelector } from './redux-utils.js';
+import { useSelector, useDispatch } from './redux-utils.js';
 import type { PlatformDetails } from '../types/device-types.js';
 import type { Endpoint } from '../types/endpoints.js';
 import type { KeyserverInfo } from '../types/keyserver-types.js';
@@ -83,7 +82,12 @@
   +keyserverInfos: { +[keyserverID: string]: KeyserverInfoPartial },
 };
 
-const bindCallKeyserverEndpointSelector = createSelector(
+const bindCallKeyserverEndpointSelector: BindKeyserverCallParams => <
+  Args: mixed,
+  Return,
+>(
+  keyserverCall: ActionFunc<Args, Return>,
+) => Args => Promise<Return> = createSelector(
   (state: BindKeyserverCallParams) => state.dispatch,
   (state: BindKeyserverCallParams) => state.currentUserInfo,
   (state: BindKeyserverCallParams) => state.keyserverInfos,
@@ -130,7 +134,7 @@
             );
           };
 
-          const promises = {};
+          const promises: { [string]: Promise<CallServerEndpoint> } = {};
           for (const keyserverID in requests) {
             promises[keyserverID] = bindCallKeyserverEndpoint(keyserverID);
           }