1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-12-10 07:22:27 +03:00

Avoid use of Optional type (#5093)

* Avoid use of Optional type

As we are likely to remove dependency on matrix-events-sdk

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tweak params

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Prettier

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2025-12-04 11:27:43 +00:00
committed by GitHub
parent 942fdf5bee
commit d3bdeb73f5
14 changed files with 39 additions and 56 deletions

View File

@@ -71,7 +71,7 @@ Unless otherwise specified, the following applies to all code:
11. If a variable is not receiving a value on declaration, its type must be defined. 11. If a variable is not receiving a value on declaration, its type must be defined.
```typescript ```typescript
let errorMessage: Optional<string>; let errorMessage: string;
``` ```
12. Objects can use shorthand declarations, including mixing of types. 12. Objects can use shorthand declarations, including mixing of types.
@@ -150,8 +150,7 @@ Unless otherwise specified, the following applies to all code:
1. When using `any`, a comment explaining why must be present. 1. When using `any`, a comment explaining why must be present.
27. `import` should be used instead of `require`, as `require` does not have types. 27. `import` should be used instead of `require`, as `require` does not have types.
28. Export only what can be reused. 28. Export only what can be reused.
29. Prefer a type like `Optional<X>` (`type Optional<T> = T | null | undefined`) instead 29. Prefer a type like `X | null` instead of truly optional parameters.
of truly optional parameters.
1. A notable exception is when the likelihood of a bug is minimal, such as when a function 1. A notable exception is when the likelihood of a bug is minimal, such as when a function
takes an argument that is more often not required than required. An example where the takes an argument that is more often not required than required. An example where the
`?` operator is inappropriate is when taking a room ID: typically the caller should `?` operator is inappropriate is when taking a room ID: typically the caller should
@@ -161,7 +160,7 @@ Unless otherwise specified, the following applies to all code:
```typescript ```typescript
function doThingWithRoom( function doThingWithRoom(
thing: string, thing: string,
room: Optional<string>, // require the caller to specify room: string | null, // require the caller to specify
) { ) {
// ... // ...
} }

View File

@@ -672,7 +672,7 @@ describe("MatrixClient event timelines", function () {
expect(timeline!.getEvents().find((e) => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy(); expect(timeline!.getEvents().find((e) => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy();
}); });
it("should return undefined when event is not in the thread that the given timelineSet is representing", () => { it("should return null when event is not in the thread that the given timelineSet is representing", () => {
// @ts-ignore // @ts-ignore
client.clientOpts.threadSupport = true; client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental); Thread.setServerSideSupport(FeatureSupport.Experimental);
@@ -696,12 +696,12 @@ describe("MatrixClient event timelines", function () {
}); });
return Promise.all([ return Promise.all([
expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id!)).resolves.toBeUndefined(), expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id!)).resolves.toBeNull(),
httpBackend.flushAllExpected(), httpBackend.flushAllExpected(),
]); ]);
}); });
it("should return undefined when event is within a thread but timelineSet is not", () => { it("should return null when event is within a thread but timelineSet is not", () => {
// @ts-ignore // @ts-ignore
client.clientOpts.threadSupport = true; client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental); Thread.setServerSideSupport(FeatureSupport.Experimental);
@@ -723,7 +723,7 @@ describe("MatrixClient event timelines", function () {
}); });
return Promise.all([ return Promise.all([
expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!)).resolves.toBeUndefined(), expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!)).resolves.toBeNull(),
httpBackend.flushAllExpected(), httpBackend.flushAllExpected(),
]); ]);
}); });

View File

@@ -19,7 +19,7 @@ limitations under the License.
*/ */
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, M_POLL_START, type Optional, PollStartEvent } from "matrix-events-sdk"; import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, M_POLL_START, PollStartEvent } from "matrix-events-sdk";
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
import { emitPromise, type IMessageOpts } from "../test-utils/test-utils"; import { emitPromise, type IMessageOpts } from "../test-utils/test-utils";
@@ -197,8 +197,8 @@ describe("Room", function () {
const addRoomThreads = ( const addRoomThreads = (
room: Room, room: Room,
thread1EventTs: Optional<number>, thread1EventTs?: number,
thread2EventTs: Optional<number>, thread2EventTs?: number,
): { thread1?: Thread; thread2?: Thread } => { ): { thread1?: Thread; thread2?: Thread } => {
const result: { thread1?: Thread; thread2?: Thread } = {}; const result: { thread1?: Thread; thread2?: Thread } = {};
@@ -4159,7 +4159,7 @@ describe("Room", function () {
}); });
it("when there is only one thread, it should return this one", () => { it("when there is only one thread, it should return this one", () => {
const { thread1 } = addRoomThreads(room, 23, null); const { thread1 } = addRoomThreads(room, 23);
expect(room.getLastThread()).toBe(thread1); expect(room.getLastThread()).toBe(thread1);
}); });

View File

@@ -98,7 +98,7 @@ function createLinkedTimelines(): [EventTimeline, EventTimeline] {
describe("TimelineIndex", function () { describe("TimelineIndex", function () {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
mockClient.getEventTimeline.mockResolvedValue(undefined); mockClient.getEventTimeline.mockResolvedValue(null);
}); });
describe("minIndex", function () { describe("minIndex", function () {
@@ -193,7 +193,7 @@ describe("TimelineWindow", function () {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
mockClient.getEventTimeline.mockResolvedValue(undefined); mockClient.getEventTimeline.mockResolvedValue(null);
mockClient.paginateEventTimeline.mockResolvedValue(false); mockClient.paginateEventTimeline.mockResolvedValue(false);
}); });

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { type EitherAnd, NamespacedValue, type Optional, UnstableValue } from "matrix-events-sdk"; import { type EitherAnd, NamespacedValue, UnstableValue } from "matrix-events-sdk";
import { isProvided } from "../extensible_events_v1/utilities.ts"; import { isProvided } from "../extensible_events_v1/utilities.ts";
@@ -125,10 +125,7 @@ export type ExtensibleEventType = NamespacedValue<string, string> | string;
* @param expected - The expected event type. * @param expected - The expected event type.
* @returns True if the given type matches the expected type. * @returns True if the given type matches the expected type.
*/ */
export function isEventTypeSame( export function isEventTypeSame(given: ExtensibleEventType | null, expected: ExtensibleEventType | null): boolean {
given: Optional<ExtensibleEventType>,
expected: Optional<ExtensibleEventType>,
): boolean {
if (typeof given === "string") { if (typeof given === "string") {
if (typeof expected === "string") { if (typeof expected === "string") {
return expected === given; return expected === given;

View File

@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { type Optional } from "matrix-events-sdk";
/** /**
* Represents a simple Matrix namespaced value. This will assume that if a stable prefix * Represents a simple Matrix namespaced value. This will assume that if a stable prefix
* is provided that the stable prefix should be used when representing the identifier. * is provided that the stable prefix should be used when representing the identifier.
@@ -62,7 +60,7 @@ export class NamespacedValue<S extends string, U extends string> {
// this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class // this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class
// so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace. // so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace.
public findIn<T>(obj: any): Optional<T> { public findIn<T>(obj: any): T | undefined {
let val: T | undefined = undefined; let val: T | undefined = undefined;
if (this.name) { if (this.name) {
val = obj?.[this.name]; val = obj?.[this.name];

View File

@@ -18,8 +18,6 @@ limitations under the License.
* This is an internal module. See {@link MatrixClient} for the public class. * This is an internal module. See {@link MatrixClient} for the public class.
*/ */
import { type Optional } from "matrix-events-sdk";
import type { IDeviceKeys, IOneTimeKey } from "./@types/crypto.ts"; import type { IDeviceKeys, IOneTimeKey } from "./@types/crypto.ts";
import { type ISyncStateData, type SetPresence, SyncApi, type SyncApiOptions, SyncState } from "./sync.ts"; import { type ISyncStateData, type SetPresence, SyncApi, type SyncApiOptions, SyncState } from "./sync.ts";
import { import {
@@ -4501,7 +4499,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: * @returns Promise which resolves:
* {@link EventTimeline} including the given event * {@link EventTimeline} including the given event
*/ */
public async getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<Optional<EventTimeline>> { public async getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<EventTimeline | null> {
// don't allow any timeline support unless it's been enabled. // don't allow any timeline support unless it's been enabled.
if (!this.timelineSupport) { if (!this.timelineSupport) {
throw new Error( throw new Error(
@@ -4519,7 +4517,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
} }
if (timelineSet.thread && this.supportsThreads()) { if (timelineSet.thread && this.supportsThreads()) {
return this.getThreadTimeline(timelineSet, eventId); return (await this.getThreadTimeline(timelineSet, eventId)) ?? null;
} }
const res = await this.getEventContext(timelineSet.room.roomId, eventId); const res = await this.getEventContext(timelineSet.room.roomId, eventId);
@@ -4533,7 +4531,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const event = mapper(res.event); const event = mapper(res.event);
if (event.isRelation(THREAD_RELATION_TYPE.name)) { if (event.isRelation(THREAD_RELATION_TYPE.name)) {
this.logger.warn("Tried loading a regular timeline at the position of a thread event"); this.logger.warn("Tried loading a regular timeline at the position of a thread event");
return undefined; return null;
} }
const events = [ const events = [
// Order events from most recent to oldest (reverse-chronological). // Order events from most recent to oldest (reverse-chronological).
@@ -4665,7 +4663,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
{ dir: Direction.Backward, from: res.start, recurse: recurse || undefined }, { dir: Direction.Backward, from: res.start, recurse: recurse || undefined },
); );
const eventsNewer: IEvent[] = []; const eventsNewer: IEvent[] = [];
let nextBatch: Optional<string> = res.end; let nextBatch = res.end;
while (nextBatch) { while (nextBatch) {
const resNewer: IRelationsResponse = await this.fetchRelations( const resNewer: IRelationsResponse = await this.fetchRelations(
timelineSet.room.roomId, timelineSet.room.roomId,
@@ -4674,7 +4672,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
null, null,
{ dir: Direction.Forward, from: nextBatch, recurse: recurse || undefined }, { dir: Direction.Forward, from: nextBatch, recurse: recurse || undefined },
); );
nextBatch = resNewer.next_batch ?? null; nextBatch = resNewer.next_batch;
eventsNewer.push(...resNewer.chunk); eventsNewer.push(...resNewer.chunk);
} }
const events = [ const events = [
@@ -4718,7 +4716,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: * @returns Promise which resolves:
* {@link EventTimeline} timeline with the latest events in the room * {@link EventTimeline} timeline with the latest events in the room
*/ */
public async getLatestTimeline(timelineSet: EventTimelineSet): Promise<Optional<EventTimeline>> { public async getLatestTimeline(timelineSet: EventTimelineSet): Promise<EventTimeline | null> {
// don't allow any timeline support unless it's been enabled. // don't allow any timeline support unless it's been enabled.
if (!this.timelineSupport) { if (!this.timelineSupport) {
throw new Error( throw new Error(

View File

@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { type Optional } from "matrix-events-sdk";
import { ExtensibleEvent } from "./ExtensibleEvent.ts"; import { ExtensibleEvent } from "./ExtensibleEvent.ts";
import { import {
type ExtensibleEventType, type ExtensibleEventType,
@@ -46,7 +44,7 @@ export class MessageEvent extends ExtensibleEvent<ExtensibleAnyMessageEventConte
/** /**
* The default HTML for the event, if provided. * The default HTML for the event, if provided.
*/ */
public readonly html: Optional<string>; public readonly html?: string;
/** /**
* All the different renderings of the message. Note that this is the same * All the different renderings of the message. Note that this is the same
@@ -82,7 +80,7 @@ export class MessageEvent extends ExtensibleEvent<ExtensibleAnyMessageEventConte
this.renderings = mmessage; this.renderings = mmessage;
} else if (isOptionalAString(mtext)) { } else if (isOptionalAString(mtext)) {
this.text = mtext; this.text = mtext;
this.html = mhtml; this.html = mhtml ?? undefined;
this.renderings = [{ body: mtext, mimetype: "text/plain" }]; this.renderings = [{ body: mtext, mimetype: "text/plain" }];
if (this.html) { if (this.html) {
this.renderings.push({ body: this.html, mimetype: "text/html" }); this.renderings.push({ body: this.html, mimetype: "text/html" });

View File

@@ -19,7 +19,7 @@ limitations under the License.
* the public classes. * the public classes.
*/ */
import { type ExtensibleEvent, ExtensibleEvents, type Optional } from "matrix-events-sdk"; import { type ExtensibleEvent, ExtensibleEvents } from "matrix-events-sdk";
import type { IEventDecryptionResult } from "../@types/crypto.ts"; import type { IEventDecryptionResult } from "../@types/crypto.ts";
import { logger } from "../logger.ts"; import { logger } from "../logger.ts";
@@ -270,7 +270,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
// addition to a falsy cached event value. We check the flag later on in // addition to a falsy cached event value. We check the flag later on in
// a public getter to decide if the cache is valid. // a public getter to decide if the cache is valid.
private _hasCachedExtEv = false; private _hasCachedExtEv = false;
private _cachedExtEv: Optional<ExtensibleEvent> = undefined; private _cachedExtEv?: ExtensibleEvent = undefined;
/** If we failed to decrypt this event, the reason for the failure. Otherwise, `null`. */ /** If we failed to decrypt this event, the reason for the failure. Otherwise, `null`. */
private _decryptionFailureReason: DecryptionFailureCode | null = null; private _decryptionFailureReason: DecryptionFailureCode | null = null;
@@ -481,9 +481,9 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* *
* @deprecated Use stable functions where possible. * @deprecated Use stable functions where possible.
*/ */
public get unstableExtensibleEvent(): Optional<ExtensibleEvent> { public get unstableExtensibleEvent(): ExtensibleEvent | undefined {
if (!this._hasCachedExtEv) { if (!this._hasCachedExtEv) {
this._cachedExtEv = ExtensibleEvents.parse(this.getEffectiveEvent()); this._cachedExtEv = ExtensibleEvents.parse(this.getEffectiveEvent()) ?? undefined;
} }
return this._cachedExtEv; return this._cachedExtEv;
} }
@@ -789,7 +789,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* @returns The user's room membership, or `undefined` if the server does * @returns The user's room membership, or `undefined` if the server does
* not report it. * not report it.
*/ */
public getMembershipAtEvent(): Optional<Membership | string> { public getMembershipAtEvent(): Membership | string | undefined {
const unsigned = this.getUnsigned(); const unsigned = this.getUnsigned();
return UNSIGNED_MEMBERSHIP_FIELD.findIn<Membership | string>(unsigned); return UNSIGNED_MEMBERSHIP_FIELD.findIn<Membership | string>(unsigned);
} }

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { M_POLL_START, type Optional } from "matrix-events-sdk"; import { M_POLL_START } from "matrix-events-sdk";
import { import {
DuplicateStrategy, DuplicateStrategy,
@@ -1196,7 +1196,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
// Get the main TimelineSet // Get the main TimelineSet
const timelineSet = this.getUnfilteredTimelineSet(); const timelineSet = this.getUnfilteredTimelineSet();
let newTimeline: Optional<EventTimeline>; let newTimeline: EventTimeline | null = null;
// If there isn't any event in the timeline, let's go fetch the latest // If there isn't any event in the timeline, let's go fetch the latest
// event and construct a timeline from it. // event and construct a timeline from it.
// //
@@ -2490,7 +2490,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
}; };
private updateThreadRootEvent = ( private updateThreadRootEvent = (
timelineSet: Optional<EventTimelineSet>, timelineSet: EventTimelineSet | undefined,
thread: Thread, thread: Thread,
toStartOfTimeline: boolean, toStartOfTimeline: boolean,
recreateEvent: boolean, recreateEvent: boolean,

View File

@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { type Optional } from "matrix-events-sdk";
import { type MatrixClient, PendingEventOrdering } from "../client.ts"; import { type MatrixClient, PendingEventOrdering } from "../client.ts";
import { TypedReEmitter } from "../ReEmitter.ts"; import { TypedReEmitter } from "../ReEmitter.ts";
import { RelationType } from "../@types/event.ts"; import { RelationType } from "../@types/event.ts";
@@ -476,7 +474,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
} }
} }
public async processEvent(event: Optional<MatrixEvent>): Promise<void> { public async processEvent(event: MatrixEvent | null | undefined): Promise<void> {
if (event) { if (event) {
this.setEventMetadata(event); this.setEventMetadata(event);
await this.fetchEditsWhereNeeded(event); await this.fetchEditsWhereNeeded(event);
@@ -686,14 +684,14 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
} }
} }
public setEventMetadata(event: Optional<MatrixEvent>): void { public setEventMetadata(event: MatrixEvent | null | undefined): void {
if (event) { if (event) {
EventTimeline.setEventMetadata(event, this.roomState, false); EventTimeline.setEventMetadata(event, this.roomState, false);
event.setThread(this); event.setThread(this);
} }
} }
public clearEventMetadata(event: Optional<MatrixEvent>): void { public clearEventMetadata(event: MatrixEvent | null | undefined): void {
if (event) { if (event) {
event.setThread(undefined); event.setThread(undefined);
delete event.event?.unsigned?.["m.relations"]?.[THREAD_RELATION_TYPE.name]; delete event.event?.unsigned?.["m.relations"]?.[THREAD_RELATION_TYPE.name];
@@ -739,7 +737,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
* A getter for the last event of the thread. * A getter for the last event of the thread.
* This might be a synthesized event, if so, it will not emit any events to listeners. * This might be a synthesized event, if so, it will not emit any events to listeners.
*/ */
public get replyToEvent(): Optional<MatrixEvent> { public get replyToEvent(): MatrixEvent | null {
return this.lastPendingEvent ?? this.lastEvent ?? this.lastReply(); return this.lastPendingEvent ?? this.lastEvent ?? this.lastReply();
} }

View File

@@ -23,8 +23,6 @@ limitations under the License.
* for HTTP and WS at some point. * for HTTP and WS at some point.
*/ */
import { type Optional } from "matrix-events-sdk";
import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend.ts"; import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend.ts";
import { User } from "./models/user.ts"; import { User } from "./models/user.ts";
import { NotificationCountType, Room, RoomEvent } from "./models/room.ts"; import { NotificationCountType, Room, RoomEvent } from "./models/room.ts";
@@ -208,7 +206,7 @@ export class SyncApi {
private readonly opts: IStoredClientOpts; private readonly opts: IStoredClientOpts;
private readonly syncOpts: SyncApiOptions; private readonly syncOpts: SyncApiOptions;
private _peekRoom: Optional<Room> = null; private _peekRoom: Room | null = null;
private currentSyncRequest?: Promise<ISyncResponse>; private currentSyncRequest?: Promise<ISyncResponse>;
private abortController?: AbortController; private abortController?: AbortController;
private syncState: SyncState | null = null; private syncState: SyncState | null = null;

View File

@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { type Optional } from "matrix-events-sdk";
import { type Direction, EventTimeline } from "./models/event-timeline.ts"; import { type Direction, EventTimeline } from "./models/event-timeline.ts";
import { logger } from "./logger.ts"; import { logger } from "./logger.ts";
import { type MatrixClient } from "./client.ts"; import { type MatrixClient } from "./client.ts";
@@ -105,7 +103,7 @@ export class TimelineWindow {
public load(initialEventId?: string, initialWindowSize = 20): Promise<void> { public load(initialEventId?: string, initialWindowSize = 20): Promise<void> {
// given an EventTimeline, find the event we were looking for, and initialise our // given an EventTimeline, find the event we were looking for, and initialise our
// fields so that the event in question is in the middle of the window. // fields so that the event in question is in the middle of the window.
const initFields = (timeline: Optional<EventTimeline>): void => { const initFields = (timeline: EventTimeline | null): void => {
if (!timeline) { if (!timeline) {
throw new Error("No timeline given to initFields"); throw new Error("No timeline given to initFields");
} }

View File

@@ -20,7 +20,6 @@ limitations under the License.
import unhomoglyph from "unhomoglyph"; import unhomoglyph from "unhomoglyph";
import promiseRetry from "p-retry"; import promiseRetry from "p-retry";
import { type Optional } from "matrix-events-sdk";
import { type IEvent, type MatrixEvent } from "./models/event.ts"; import { type IEvent, type MatrixEvent } from "./models/event.ts";
import { M_TIMESTAMP } from "./@types/location.ts"; import { M_TIMESTAMP } from "./@types/location.ts";
@@ -115,7 +114,7 @@ export function decodeParams(query: string): Record<string, string | string[]> {
* variables with. E.g. `{ "$bar": "baz" }`. * variables with. E.g. `{ "$bar": "baz" }`.
* @returns The result of replacing all template variables e.g. '/foo/baz'. * @returns The result of replacing all template variables e.g. '/foo/baz'.
*/ */
export function encodeUri(pathTemplate: string, variables: Record<string, Optional<string>>): string { export function encodeUri(pathTemplate: string, variables: Record<string, string | null | undefined>): string {
for (const key in variables) { for (const key in variables) {
if (!variables.hasOwnProperty(key)) { if (!variables.hasOwnProperty(key)) {
continue; continue;