diceline-chartmagnifiermouse-upquestion-marktwitter-whiteTwitter_Logo_Blue

Today I Learned

How to dynamically infer TypeScript single object types from tRPC query outputs

This is how you can dynamically infer a single object type from a tRPC Query returning an array of elements of that specific type:

export const exampleRouter = router({
  exampleQuery: exampleQueryImplementation,
});

// extracting a single object type from an array of that type 
export type ArrayElement<ArrayType extends unknown[] | null> =
  ArrayType extends (infer ElementType)[] ? ElementType : never;

// the output types of a specific router, indexable by query identifiers
type RouterOutput = inferRouterOutputs<typeof exampleRouter>;

// return type of a single query
export type QueryOutputArray = RouterOutput['exampleQuery'];

// the type of each element from the returned array
export type QueryOutputObject = ArrayElement<QueryOutputArray>;

Meteor.js powered React hook for retrieving articles with error and loading state handling

The custom hook created uses Meteor's useTracker hook and the subscribe method to subscribe to the article publication, as well as the ArticlesCollection to find the article with the matching _id (which is the same as the slug in this case).

const useArticle = (slug: string): any => {
    const [error, setError] = useState<Error | null>(null)

    return useTracker(() => {
        const sub = Meteor.subscribe('article', { _id: slug })
        if (sub.ready()) {
            const article = ArticlesCollection.findOne({ _id: slug })
            if (article) {
                return {
                    loading: false,
                    error: null,
                    article,
                }
            } else {
                setError(new Error(`Article with slug "${slug}" not found`))
                return {
                    loading: false,
                    error,
                    article: undefined,
                }
            }
        } else {
            return {
                loading: true,
                error: null,
                article: undefined,
            }
        }
    }, [slug, setError])
}

This hook can be used in a React component to display the article, its loading state and error:

const Article = ({ slug }: { slug: string }) => {
    const { loading, error, article } = useArticle(slug)

    if (loading) {
        return <div>Loading...</div>
    }

    if (error) {
        return <div>Error: {error.message}</div>
    }

    return (
        <div>
            <h1>{article.title}</h1>
            <p>{article.body}</p>
        </div>
    )
}

Improving mapping performance with web workers

We can use web workers to execute the computationally intensive mapping operation in a separate thread. This can be done by following these steps:

  1. Create a new TS file for the worker
// worker.ts

// define the input and output types
type WorkerInput<T, U> = { data: T, mapFn: (x: T) => U };
type WorkerOutput<U> = { data: U[] };

// function for the actual work on the data
function doWork<T, U>(input: WorkerInput<T, U>): Promise<WorkerOutput<U>> {
  // Apply the mapping function to the input data.
  const mappedData = input.data.map(input.mapFn);

  // Return the mapped data as a promise.
  return Promise.resolve({ data: mappedData });
}
  1. Use the worker in the main.ts file
// main.ts
const worker = new Worker('worker.ts');

// Define the mapping function.
const mapFn = (x: number) => x * x;

// Send data and the mapping function to the worker for processing.
worker.postMessage({ data: [1, 2, 3], mapFn });

// Listen for the message with the result from the worker
worker.addEventListener('message', (event) => {
    console.log(`Received message from worker: ${event.data}`)
});

How to type-safely interact with Firestore documents

The problem

When using Typescript and Firestore, we usually have to do a lot of manual casting when working with documents. One such example would be getting the data of a document:

const thread = threadDocument.data(); // this will be of type any

Should we want to interact with the data in a type-safe manner, we'll have to cast it, which can quickly become tedious.

const thread = <ThreadData>threadDocument.data();

Additionally, when we write data to Firestore, there are no restrictions on how the data should look.

The solution

This is when Firestore Data Converters can come in handy. All we have to do is implement two methods - one where we constrain the data that gets written and one where we cast the data coming from Firestore:

const converter = {
  toFirestore: (dataToBeWritten: ThreadData) => data,
  fromFirestore: (document: QueryDocumentSnapshot) => <ThreadData>document.data(),
};

To take this one step further, we can store the "converted" collection reference so we won't have to apply the converters each time we query the collection:

const threadCollection = db.collection("threads").withConverter(converter);

Now we can safely interact with the collection without having to cast the data:

const threadDocument = await threadCollection.doc(id).get();
const thread = threadDocument.data(); // this will be of type ThreadData

How to obtain reactivity in custom hooks while interacting with the local storage

This is how we can obtain reactivity in our custom React.js hooks while working with the local storage, using the Pub/Sub (Observer) design pattern (with TypeScript support).

The goal is to implement a "useLocalStorage" custom hook, which will abstract away the complexity of reading from and writing to the local storage. As we know, each custom hook instantiates its own state. That is a problem in our case because when one instance of the hook updates the local storage, the state copies held by all the other hook instances will be out of sync and will never be updated.

We can solve this issue using the following idea: we can mimic a centralized shared state between our custom hook instances by delegating the responsibility of holding these in sync with the local storage to a custom "manager", the Observer object.

Our custom hook will work based on these ideas:

  1. Custom Hook instances will subscribe their inner state updater functions to this manager
  2. Custom Hook instances will publish the new state to the manager when updating a key of the local storage
  3. The manager will trigger all subscriber functions and thus update the inner states of the custom hook instances.

The observer object:

export type Listener<EventType> = (event: EventType) => void;

export type ObserverReturnType<KeyType, EventType> = {
  subscribe: (entryKey: KeyType, listener: Listener<EventType>) => () => void;
  publish: (entryKey: KeyType, event: EventType) => void;
};

export default function createObserver<
  KeyType extends string | number | symbol,
  EventType,
>(): ObserverReturnType<KeyType, EventType> {
  const listeners: Record<KeyType, Listener<EventType>[]> = {} as Record<
    KeyType,
    Listener<EventType>[]
  >;

  return {
    subscribe: (entryKey: KeyType, listener: Listener<EventType>) => {
      if (!listeners[entryKey]) listeners[entryKey] = [];
      listeners[entryKey].push(listener);
      return () => {
        listeners[entryKey].splice(listeners[entryKey].indexOf(listener), 1);
      };
    },
    publish: (entryKey: KeyType, event: EventType) => {
      if (!listeners[entryKey]) listeners[entryKey] = [];
      listeners[entryKey].forEach((listener: Listener<EventType>) =>
        listener(event),
      );
    },
  };
}

export const LocalStorageObserver = createObserver<
  LOCAL_STORAGE_KEYS,
  string
>();

export const { subscribe, publish } = LocalStorageObserver;

The useLocalStorage custom hook (window checks are optional, depending on which environment this JavaScript will run on):

export function useLocalStorage<T>(key: LOCAL_STORAGE_KEYS, initialValue: T) {
  const [storedValue, setStoredValue] = useState(() => {
    if (typeof window === 'undefined') {
      return initialValue;
    }
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  LocalStorageObserver.subscribe(key, setStoredValue);

  const setValue = (value: T) => {
    try {
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      LocalStorageObserver.publish(key, valueToStore);
      if (typeof window !== 'undefined') {
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
      }
    } catch (error) {
      console.error(error);
    }
  };
  return [storedValue, setValue];
}