Hone logo
Hone
Problems

Type-Level Lists in TypeScript

Type-level programming allows us to perform computations and manipulations at compile time, rather than runtime. Implementing type-level lists in TypeScript enables us to represent sequences of types and perform operations on them, such as concatenation, length calculation, and filtering, all during compilation. This challenge will guide you through building the foundational elements of type-level lists.

Problem Description

The goal is to implement a basic type-level list structure in TypeScript using conditional types and recursive types. You will need to define types for constructing lists, checking their length, and concatenating them. The type-level list should be immutable; operations should return new lists rather than modifying existing ones. Consider empty lists and lists containing a single type.

Key Requirements:

  • List<T> Type: Define a generic type List<T> that represents a type-level list. An empty list should be represented by []. A non-empty list should be a recursive type.
  • prepend<T, U> Type: Create a type prepend<T, U> that takes two types, T and U, and returns a new List<T> with U prepended to it.
  • length<T> Type: Define a type length<T> that calculates the length of a List<T>. The length should be represented as a number type.
  • concat<T, U> Type: Create a type concat<T, U> that takes two List<T> and List<U> and returns a new List<T | U>.

Expected Behavior:

The types should be structurally sound and produce the expected results when used in type assertions. The length type should accurately reflect the number of elements in the list. concat should combine the lists correctly.

Edge Cases to Consider:

  • Empty lists: Ensure your code handles empty lists gracefully in all operations.
  • Lists of different lengths: concat should work correctly regardless of the lengths of the input lists.
  • Type safety: The types should be type-safe and prevent invalid operations.

Examples

Example 1:

type List1 = List<number>;
type List2 = prepend<number, string>;
type List3 = prepend<List1, boolean>;

// List1 should be equivalent to []
// List2 should be equivalent to [string, number]
// List3 should be equivalent to [boolean, List<number>]

Example 2:

type ListA = List<string>;
type ListB = prepend<ListA, number>;
type ListC = prepend<ListB, boolean>;

type Concatenated = concat<ListC, List<number>>;
// Concatenated should be equivalent to [boolean, number, string, number]

Example 3:

type EmptyList = [];
type LengthOfEmpty = length<EmptyList>; // LengthOfEmpty should be 0

type SingleElementList = prepend<number, string>;
type LengthOfSingle = length<SingleElementList>; // LengthOfSingle should be 1

Constraints

  • The solution must be implemented using TypeScript's type system (conditional types, recursive types, etc.).
  • No runtime code is allowed. This is purely a type-level exercise.
  • The length type must return a number type.
  • The solution should be reasonably efficient in terms of type checking time (avoiding excessively complex or deeply recursive types that could lead to compiler errors).

Notes

  • Think about how to represent the "end" of a list to allow for length calculation. A common approach is to use a distinct type to mark the end.
  • Start with the List and prepend types, then move on to length and concat.
  • Test your types thoroughly with various inputs to ensure they behave as expected. Use the type-fest library or similar tools to help with type assertions and testing.
  • Consider how you can make your List type generic and reusable for different types.
Loading editor...
typescript