Hone logo
Hone
Problems

Simulating Higher-Kinded Types in TypeScript

Higher-kinded types are a powerful feature found in languages like Haskell and Scala, allowing you to abstract over type constructors (like List, Maybe, or IO). TypeScript doesn't natively support them, but we can simulate their behavior using generics and conditional types. This challenge asks you to create a system that mimics the core functionality of higher-kinded types, enabling you to abstract over container types.

Problem Description

The goal is to create a Container type that acts as a higher-kinded type. This Container type should accept a type constructor F (representing the container type) and a type A (the type held within the container). You'll then implement two functions: map and extract.

  • map<B>(f: (a: A) => B): Container<F, B>: This function should take a function f that transforms the inner type A to a new type B. It should apply this function to the value inside the container and return a new container of type F holding the transformed value B. The container type F remains unchanged.
  • extract<B>(container: Container<F, B>): A: This function should extract the inner type A from the container. It's a utility function to get the original value out.

Essentially, you're building a system that allows you to work with values inside containers without knowing the specific container type at compile time. This is useful for abstracting over data structures and side-effectful operations.

Examples

Example 1:

type Maybe<A> = { type: 'Some', value: A } | { type: 'None' };

type Container<F, A> = F extends (a: infer B) => any ? { type: 'Container', value: B } : never;

const someValue: Container<typeof Maybe, number> = { type: 'Container', value: { type: 'Some', value: 10 } };

const mappedValue: Container<typeof Maybe, string> = map(value => `The value is: ${value}`, someValue);

// mappedValue will be of type { type: 'Container', value: { type: 'Some', value: 'The value is: 10' } }

const extractedValue: number = extract(someValue);

// extractedValue will be 10

Example 2:

type List<A> = A[];

type Container<F, A> = F extends (a: infer B) => any ? { type: 'Container', value: B } : never;

const myList: Container<typeof List, string> = { type: 'Container', value: ['a', 'b', 'c'] };

const mappedList: Container<typeof List, number> = map(str => str.length, myList);

// mappedList will be of type { type: 'Container', value: [3, 1, 1] }

const extractedList: string[] = extract(myList);

// extractedList will be ['a', 'b', 'c']

Example 3: (Edge Case - Empty List)

type List<A> = A[];

type Container<F, A> = F extends (a: infer B) => any ? { type: 'Container', value: B } : never;

const emptyList: Container<typeof List, number> = { type: 'Container', value: [] };

const mappedEmptyList: Container<typeof List, number> = map(num => num * 2, emptyList);

// mappedEmptyList will be of type { type: 'Container', value: [] }

const extractedEmptyList: number[] = extract(emptyList);

// extractedEmptyList will be []

Constraints

  • The Container type must be defined using conditional types and generics.
  • The map function must return a new container of the same container type F but with the transformed value.
  • The extract function must correctly extract the original value of type A from the container.
  • The solution should be type-safe. TypeScript should be able to infer the correct types for map and extract without explicit type annotations (where possible).
  • The solution should work with any type constructor F that can be represented as a function taking a single argument.

Notes

  • Think about how to use conditional types to infer the inner type A from the container type F.
  • The F extends (a: infer B) => any ? { type: 'Container', value: B } : never pattern is key to inferring the type held within the container.
  • This is a simulation of higher-kinded types, so you're not actually creating a new language feature. You're using TypeScript's type system to achieve a similar effect.
  • Consider how to handle different container types (e.g., Maybe, List, Result) consistently.
  • Focus on the type-level logic; you don't need to implement any runtime behavior. The functions map and extract are purely type-level operations.
Loading editor...
typescript