Joshua Pendragon

Projects

This is a collection of projects that I have worked on.

  • This website.
    This website is built using Astro, Tailwind CSS, and Markdown. It is hosted on Netlify.
  • V1 of this Website
    The first version of this website, built using Rust, Axum, and Askama.
  • Playwright JSON Summary Reporter
    A custom reporter for Playwright that outputs a JSON summary of test results. I contributed to this project by adding a new feature that allows the user to specify a custom output file path.
  • Playwright Project Showcase
    A Playwright test project showcase. Most of this code was used in or inspired by my professional work as an SDET. Key features include: custom, composed fixtures, multi-environment support with setup and teardown, and a custom reporter that outputs a JSON summary of test results for use in other integrations.
  • OvationCXM
    A customer experience management platform that helps businesses collect and analyze customer feedback. I contributed to this project by triaging (and sometimes fixing) bugs, and was the primary developer for the automated testing framework.

Gists

This is a collection of gists that I have created.

  • Typescript - Recursive Partial Type

    The provided TypeScript code defines a utility type RecursivePartial. This type is used to create a version of an existing type, but with all of its properties (and their nested properties) made optional and able to be partially provided.

    The RecursivePartial type is defined as a mapped type that iterates over all properties (Prop) of a given type (Type):

    export type RecursivePartial<Type> = {
      [Prop in keyof Type]?: ...
    };

    For each property, it checks if the property type is an array or an object. If the property type is an array, it infers the array's item type (U) and applies RecursivePartial to it, making all properties of the item type optional:

    Type[Prop] extends (infer U)[]
      ? RecursivePartial<U>[]
      : ...

    If the property type is an object (or undefined), it applies RecursivePartial to the property type itself, making all properties of the object type optional:

    Type[Prop] extends object | undefined
      ? RecursivePartial<Type[Prop]>
      : ...

    If the property type is neither an array nor an object, it leaves the property type as is:

    : Type[Prop];

    In summary, the RecursivePartial type can be used to create a version of an existing type with all properties (including nested properties) made optional. This can be useful in scenarios where you want to partially update an object or when you're dealing with data that might not be fully provided.


    The RecursivePartial utility type can be used in scenarios where you want to make all properties of an object (including nested properties) optional. Here are a few practical use cases:

    1. Partial Updates: When you want to partially update an object, you can use RecursivePartial to create a type for the update object. This allows you to provide only the properties that you want to update, without having to provide all properties of the object.
    type UserUpdate = RecursivePartial<User>;
    1. Configuration Objects: In applications, you often have large configuration objects that have default values. When setting the configuration, you might want to override only some of the properties. RecursivePartial can be used to create a type for the configuration override object.
    type ConfigOverride = RecursivePartial<Config>;
    1. Test Data: When writing tests, you often need to create objects that represent test data. With RecursivePartial, you can create a type for the test data object that allows you to provide only the properties that are relevant to the test.
    type TestProduct = RecursivePartial<Product>;

    In all these cases, RecursivePartial helps to make your code more flexible and easier to work with by allowing you to work with partial objects.

    export type RecursivePartial<Type> = {
    [Prop in keyof Type]?: Type[Prop] extends (infer U)[]
    ? RecursivePartial<U>[]
    : Type[Prop] extends object | undefined
    ? RecursivePartial<Type[Prop]>
    : Type[Prop];
    };
  • Typescript - 'One of' Type

    The provided TypeScript code defines a utility type OneOfType that ensures an object can only have one property from the provided type. This is achieved using TypeScript's conditional and mapped types.

    The OneOfType type is defined as follows:

    export type OneOfType<T> = ValueOf<OneOfByKey<T>>;

    Here, OneOfType is a mapped type that transforms an object type T into a type that allows only one property from T to be present at a time. It uses two helper types OneOfByKey and ValueOf.

    The OneOfByKey type is a mapped type that transforms each property in T into a type that allows only that property to be present:

    type OneOfByKey<T> = {[key in keyof T]: OneOnly<T, key>};

    The OneOnly type is a conditional type that takes an object type Obj and a key Key and produces a type that allows only the property Key to be present in Obj:

    type OneOnly<Obj, Key extends keyof Obj> = {
      [key in Exclude<keyof Obj, Key>]+?: undefined;
    } & Pick<Obj, Key>;

    Here, Exclude<keyof Obj, Key> produces a type that represents all keys of Obj except Key, and Pick<Obj, Key> produces a type that includes only the property Key from Obj.

    The ValueOf type is a simple helper type that takes an object type Obj and produces a type that represents the values of Obj:

    type ValueOf<Obj extends object> = Obj[keyof Obj];

    In summary, this code defines a utility type OneOfType that can be used to ensure an object can only have one property from a given type. This can be useful in situations where you want to enforce that an object can only have one of several possible properties.


    The OneOfType utility type can be used in scenarios where you want to enforce that an object can only have one of several possible properties. Here are a few practical use cases:

    1. API Responses: When dealing with API responses, sometimes the response object can have different properties based on the status of the response. For example, a successful response might have a data property, while an error response might have an error property. You can use OneOfType to enforce that the response object can only have one of these properties.
    type ApiResponse = OneOfType<{ data: Data; error: Error }>;
    1. Form Fields: In a form, you might have different types of fields (text, number, date, etc.), each represented by an object with different properties. You can use OneOfType to ensure that a field object can only have the properties of one field type.
    type FormField = OneOfType<{ textField: TextField; numberField: NumberField; dateField: DateField }>;
    1. Union Types: If you have a union type where each type in the union has a unique property, you can use OneOfType to create a type that represents an object that can have the unique property of any type in the union, but not more than one.
    type UnionType = OneOfType<{ typeA: TypeA; typeB: TypeB; typeC: TypeC }>;

    In all these cases, OneOfType helps to enforce a certain structure on your objects, making your code more type-safe and easier to reason about.

    /**
    * A custom utility type to allow an object to be only one Type from the provided
    * Type.
    * @example
    * type OneOf = OneOfType<{userId:string, userEmail:string}>;
    * //allows {userId:string} || {userEmail:string}, but not both
    */
    export type OneOfType<T> = ValueOf<OneOfByKey<T>>;
    type OneOfByKey<T> = {[key in keyof T]: OneOnly<T, key>};
    type OneOnly<Obj, Key extends keyof Obj> = {
    [key in Exclude<keyof Obj, Key>]+?: undefined;
    } & Pick<Obj, Key>;
    type ValueOf<Obj extends object> = Obj[keyof Obj];
  • Typescript - Type-safe Entries

    The TypeScript code snippet provided is a utility for working with objects in a type-safe manner. It defines a type Entries and a function getEntries.

    The Entries type is a mapped type that transforms the properties of an object type T into an array of tuples. Each tuple contains the property key as its first element and the corresponding property value as its second element.

    export type Entries<T> = {
      [K in keyof T]-?: [K, T[K]];
    }[keyof T][];

    The -? operator is used to make optional properties in T required in the Entries type. This ensures that every property of T will be included in the Entries type, even if it is optional in T.

    The getEntries function takes an object of type T and returns its entries as an array of tuples. The return type of the function is Entries<T>, ensuring that the returned array matches the structure defined by the Entries type.

    export const getEntries = <T extends object>(obj: T): Entries<T> => {
      return Object.entries(obj) as Entries<T>;
    };

    The Object.entries method is used to get the entries of the object, but since it returns an array of [string, unknown][], it is type casted to Entries<T> to ensure type safety.

    In summary, this code provides a type-safe way of getting the entries of an object in TypeScript. The Entries type and getEntries function can be used together to work with object entries while preserving the original types of the keys and values.

    view raw entries.md hosted with ❤ by GitHub
    export type Entries<T> = {
    [K in keyof T]-?: [K, T[K]];
    }[keyof T][];
    export const getEntries = <T extends object>(obj: T): Entries<T> => {
    return Object.entries(obj) as Entries<T>;
    };
    const exampleObj = {
    name: 'John',
    age: 30,
    location: 'New York',
    };
    const entries = getEntries(exampleObj);
    // type of entries is:
    // type Entries = ["name", string] | ["age", number] | ["location", string]
    const entries2 = Object.entries(exampleObj);
    // type of entries2 is:
    // type Entries2 = [keyof typeof exampleObj, string | number][]
    view raw entries.ts hosted with ❤ by GitHub
  • Typescript - Branded Types

    Branded Types

    A "Branded" type is a type that is a subtype of the original type, but has a unique literal value in a common field (the brand). This allows us to define types that are more specific than the original type, but are still compatible with it. For example, we have the type EmailAddress, which is a string that is guaranteed to be a valid email address.

    Branded types can only be created by calling the brand function, which takes a value of the original type and returns a value of the branded type.

    Usage

    Creating a new Branded type

    Branded types are composed of a union of their original type and a brand object.

    EG:

    type EmailAddress = string & { __brand: "EmailAddress" };
    type;

    Branded types should be grouped by their common base type, and should only consist of that type and the brand object.

    export type EmailAddress = string & {__brand: 'emailAddress'};
    /**
    * EmailAddress assertion function
    */
    export function assertIsEmailAddress(
    emailAddress: unknown
    ): asserts emailAddress is EmailAddress {
    if (!isEmailAddress(emailAddress)) {
    throw new Error(`Expected ${emailAddress} to be a valid email address`);
    }
    }
    /**
    * Email address type check function
    */
    export function isEmailAddress(
    emailAddress: unknown
    ): emailAddress is EmailAddress {
    const emailRegex = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/;
    return typeof emailAddress === 'string' && emailRegex.test(emailAddress);
    }
    view raw email.ts hosted with ❤ by GitHub
    interface Credentials {
    login: string;
    password: string;
    }
    export type UserTypes = 'orgAdmin' | 'superAdmin' | 'standardUser';
    export type BrandedCredentials<T extends UserTypes> = Credentials & {
    __brand: T;
    };
    export type OrgAdmin = BrandedCredentials<'orgAdmin'>;
    export type SuperAdmin = BrandedCredentials<'superAdmin'>;
    export type StandardUser = BrandedCredentials<'standardUser'>;
    export function assertIsOrgAdmin(
    credentials: unknown
    ): asserts credentials is OrgAdmin {
    if (!isOrgAdmin(credentials)) {
    throw new Error('Not an OrgAdmin');
    }
    }
    export function assertIsSuperAdmin(
    credentials: unknown
    ): asserts credentials is SuperAdmin {
    if (!isSuperAdmin(credentials)) {
    throw new Error('Not a SuperAdmin');
    }
    }
    export function assertIsStandardUser(
    credentials: unknown
    ): asserts credentials is StandardUser {
    if (!isStandardUser(credentials)) {
    throw new Error('Not a StandardUser');
    }
    }
    export function isOrgAdmin(credentials: unknown): credentials is OrgAdmin {
    return (credentials as OrgAdmin).__brand === 'orgAdmin';
    }
    function isSuperAdmin(credentials: unknown): credentials is SuperAdmin {
    return (credentials as SuperAdmin).__brand === 'superAdmin';
    }
    export function isStandardUser(
    credentials: unknown
    ): credentials is StandardUser {
    return (credentials as StandardUser).__brand === 'standardUser';
    }
    view raw users.ts hosted with ❤ by GitHub
    export type UUID = string & {__brand: 'UUID'};
    /**
    * Hex string in the format of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
    */
    export const UUIDREGEX =
    /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/;
    /**
    * UUID assertion function
    * Hex string in the format of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
    */
    export function assertUUID(uuid: string): asserts uuid is UUID {
    if (!isUUID(uuid)) {
    throw new Error(`Expected ${uuid} to be a valid UUID`);
    }
    }
    /**
    * UUID type check function
    * Hex string in the format of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
    */
    export function isUUID(uuid: string): uuid is UUID {
    return uuid.match(UUIDREGEX) !== null;
    }
    view raw uuid.ts hosted with ❤ by GitHub
  • Typescript - Retry Wrapper

    retryUntilSuccess Function

    The retryUntilSuccess function is a utility function that allows you to retry a given function until it succeeds or until a maximum number of attempts has been reached.

    Signature

    function retryUntilSuccess<Fn extends RetryFunction>(f: Fn, n: number, ...args: Parameters<Fn>): boolean

    Parameters

    • f: The function to be executed. It should accept any number of arguments and return a boolean value indicating success (true) or failure (false).
    • n: The maximum number of times the function f should be retried.
    • ...args: The arguments to pass to the function f. These arguments are inferred from the type signature of f.

    Returns

    Returns true if the function f returns true within the specified number of attempts. Throws an error with a message detailing the failure if the function continues to fail after n attempts.

    Example

    Here's an example usage of retryUntilSuccess:

    function maybeSucceeds(lbound: number, ubound: number, top: number) {
      const randomNumber = Math.random() * top;
      return randomNumber > lbound && randomNumber < ubound;
    }
    
    const retriedResults = [];
    for (let i = 0; i < 5; i++) {
      retriedResults.push(retryUntilSuccess(maybeSucceeds, 5, 1, 5, 10));
    }

    In this example, maybeSucceeds is a function that randomly generates a number and checks if it falls within a certain range. The retryUntilSuccess function is used to keep trying this operation until it succeeds or until five attempts have been made.

    type RetryFunction = (...args: any[]) => boolean;
    function retryUntilSuccess<Fn extends RetryFunction>(f: Fn, n: number, ...args: Parameters<Fn>): boolean {
    let attempts = 0;
    while (attempts < n) {
    if (f(...args)) {
    return true;
    }
    attempts++;
    }
    throw new Error(`Function ${f.name} failed after ${n} attempts.`);
    }
    // example
    function maybeSucceeds(lbound: number, ubound: number, top: number) {
    const randomNumber = Math.random() * top;
    return randomNumber > lbound && randomNumber < ubound
    }
    const results = [];
    for (let i = 0; i < 5; i++) {
    results.push(maybeSucceeds(1, 5, 10));
    }
    results
    const retriedResults = [];
    for (let i = 0; i < 5; i++) {
    retriedResults.push(retryUntilSuccess(maybeSucceeds, 5, 1, 5, 10));
    }
    retriedResults // should pretty much always work
    const retriedResults2 = [];
    for (let i = 0; i < 5; i++) {
    retriedResults2.push(retryUntilSuccess(maybeSucceeds, 5, 1, 0, 10));
    }
    retriedResults2 // should throw "Function maybeSucceeds failed after 5 attempts" (impossible to succeed)