Hone logo
Hone
Problems

Building a Generic Indexed Types Framework in TypeScript

This challenge focuses on creating a flexible and reusable framework for defining and accessing indexed types in TypeScript. This is a common pattern for managing collections of data where each item is uniquely identified by a string key, enabling efficient lookup and manipulation.

Problem Description

Your task is to design and implement a TypeScript type utility that allows you to represent and work with indexed collections of data. This framework should be generic enough to handle different types of values within the collection and support common operations like adding new items, retrieving items by their index, and checking for the existence of an index.

Key Requirements:

  1. IndexedCollection<T> Type: Create a generic type IndexedCollection<T> that represents a collection where T is the type of the values stored. The keys of this collection will be string.
  2. add<K extends string, V>(collection: IndexedCollection<T>, key: K, value: V): IndexedCollection<T & { [key in K]: V }> Function: Implement a function add that takes an existing IndexedCollection, a string key, and a value. It should return a new IndexedCollection with the added key-value pair. The returned type should accurately reflect the inclusion of the new key and its associated value.
  3. get<K extends string>(collection: IndexedCollection<T>, key: K): T | undefined Function: Implement a function get that takes an IndexedCollection and a string key. It should return the value associated with that key, or undefined if the key does not exist.
  4. Type Safety: All operations must be type-safe. The add function's return type should be a union of the original collection type and the new key-value pair. The get function should return the correct type of the value or undefined.

Expected Behavior:

  • The IndexedCollection should behave like an object with string keys and values of type T.
  • The add function should create a new object that is a superset of the original, including the newly added property.
  • The get function should provide runtime access to the values, with type information reflecting the potential absence of a key.

Edge Cases to Consider:

  • Adding a key that already exists in the collection. The type system should handle this gracefully, potentially overwriting the previous type with the new one, but the runtime behavior should be a simple assignment.
  • Retrieving a key that is not present in the collection.

Examples

Example 1: Initializing and Adding

// Initial empty indexed collection
const myCollection: IndexedCollection<{}> = {};

// Add a 'name' property
const collectionWithName = add(myCollection, 'name', 'Alice');
// collectionWithName type: IndexedCollection<{ name: string }>

// Add an 'age' property
const collectionWithAge = add(collectionWithName, 'age', 30);
// collectionWithAge type: IndexedCollection<{ name: string } & { age: number }>

// Add a 'isActive' property
const finalCollection = add(collectionWithAge, 'isActive', true);
// finalCollection type: IndexedCollection<{ name: string } & { age: number } & { isActive: boolean }>

console.log(finalCollection);
// Expected runtime output: { name: 'Alice', age: 30, isActive: true }

Example 2: Retrieving Values

// Assuming finalCollection from Example 1

const name = get(finalCollection, 'name'); // type: string | undefined
const age = get(finalCollection, 'age');   // type: number | undefined
const isActive = get(finalCollection, 'isActive'); // type: boolean | undefined
const city = get(finalCollection, 'city'); // type: undefined

console.log(name); // Expected runtime output: Alice
console.log(age);  // Expected runtime output: 30
console.log(isActive); // Expected runtime output: true
console.log(city); // Expected runtime output: undefined

Example 3: Overwriting a Key (Type vs. Runtime)

const initialData: IndexedCollection<{ id: number }> = { id: 123 };

// Adding a key that already exists
const updatedData = add(initialData, 'id', 'abc');
// updatedData type: IndexedCollection<{ id: string }> - The type of 'id' is now string.

const idValue = get(updatedData, 'id'); // type: string | undefined

console.log(idValue); // Expected runtime output: abc

Constraints

  • The IndexedCollection type must be an object literal with string keys.
  • The add function must return a new object; it should not mutate the original collection.
  • The add function's return type must precisely reflect the union of the original type and the new property.
  • The get function must return T | undefined where T is the type of the value associated with the key.

Notes

  • Think about how to use TypeScript's conditional types and intersection types to achieve the dynamic type transformations in the add function.
  • Consider using mapped types to help define the structure of your IndexedCollection.
  • The runtime implementation of add can use object spread syntax.
  • The get function's return type is crucial for ensuring type safety when accessing elements.
Loading editor...
typescript