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 functionfthat transforms the inner typeAto a new typeB. It should apply this function to the value inside the container and return a new container of typeFholding the transformed valueB. The container typeFremains unchanged.extract<B>(container: Container<F, B>): A: This function should extract the inner typeAfrom 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
Containertype must be defined using conditional types and generics. - The
mapfunction must return a new container of the same container typeFbut with the transformed value. - The
extractfunction must correctly extract the original value of typeAfrom the container. - The solution should be type-safe. TypeScript should be able to infer the correct types for
mapandextractwithout explicit type annotations (where possible). - The solution should work with any type constructor
Fthat can be represented as a function taking a single argument.
Notes
- Think about how to use conditional types to infer the inner type
Afrom the container typeF. - The
F extends (a: infer B) => any ? { type: 'Container', value: B } : neverpattern 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
mapandextractare purely type-level operations.