Hone logo
Hone
Problems

Implementing Liquid Types in TypeScript

Liquid types, popularized by Remy Sharp, offer a powerful way to create flexible and reusable type definitions in TypeScript. They allow you to define types that can be dynamically constructed based on other types, enabling advanced type manipulation and conditional type logic. This challenge asks you to implement a simplified version of liquid types to demonstrate the core concepts.

Problem Description

The goal is to create a set of utility types that mimic the behavior of liquid types. Specifically, you'll implement three core liquid type operations: pipe, compact, and merge. These operations allow you to chain type transformations, filter out null and undefined values, and combine multiple types into a single type, respectively.

Key Requirements:

  • pipe<T>(...types: T[]): This function takes a variable number of types as arguments and returns a single type that is the intersection of all input types. Essentially, it combines all input types into one.
  • compact<T>(type: T): This function takes a type T and returns a type that excludes null and undefined from T. If T is already null or undefined, it should return never.
  • merge<T>(type1: T, type2: T): This function takes two types T and returns a type that is the union of type1 and type2.

Expected Behavior:

The utility types should behave as expected when used in type annotations. The compiler should be able to infer the correct types based on the usage of these functions.

Edge Cases to Consider:

  • pipe with no arguments should return never.
  • compact with null or undefined as input should return never.
  • merge should handle primitive types correctly.
  • Consider how these types interact with other TypeScript features like generics and conditional types.

Examples

Example 1: pipe

type Result = pipe<string, number, boolean>(); // type Result = string & number & boolean

Example 2: compact

type MaybeString = string | null | undefined;
type StringOnly = compact<MaybeString>; // type StringOnly = string

Example 3: merge

type TypeA = { a: string };
type TypeB = { b: number };
type CombinedType = merge<TypeA, TypeB>; // type CombinedType = { a: string; b: number; }

Example 4: pipe with compact

type MaybeNumber = number | null | undefined;
type CompactedNumber = pipe<MaybeNumber, compact<MaybeNumber>>; // type CompactedNumber = number

Constraints

  • The solution must be written in TypeScript.
  • The utility types must be defined using type aliases.
  • The solution should be as concise and readable as possible.
  • The solution should be type-safe and avoid any runtime errors.
  • The solution should handle all the edge cases described above.

Notes

  • Liquid types are primarily a compile-time feature, so there's no runtime code to execute. The focus is on creating correct and useful type definitions.
  • Consider using conditional types and intersection/union types to implement the desired behavior.
  • Think about how to handle the case where a type is already never.
  • This is a simplified implementation; real-world liquid types often involve more complex operations and features. The goal here is to understand the core principles.
Loading editor...
typescript