Hone logo
Hone
Problems

Implementing Phantom Types in TypeScript

Phantom types are a powerful technique in statically-typed languages like TypeScript that allow you to encode information about a value at compile time that is not present at runtime. This can be used to enforce constraints, prevent errors, and improve code clarity by ensuring that values are used in the correct context. This challenge asks you to implement a basic phantom typing system using generics and conditional types.

Problem Description

You are tasked with creating a system for representing values associated with a specific "stage" or "context." Imagine you're building a state machine where a value's validity depends on the current state. You'll create a generic type Stage<T, StageName> that takes a value T and associates it with a StageName (a string literal type). The key is that the StageName should not be present at runtime; it's purely a compile-time construct to enforce type safety.

Specifically, you need to implement the following:

  1. Stage<T, StageName> Type: This type should accept a value T and a StageName (string literal type). It should return a type that represents the value T tagged with the StageName.
  2. getStageValue<T, StageName>(stage: Stage<T, StageName>): T Function: This function should take a Stage<T, StageName> as input and return the underlying value T. The StageName should be discarded during this process.
  3. Type Safety: The system should enforce that values associated with different stages are treated differently. Attempting to use a value from one stage in a context expecting a value from another stage should result in a compile-time error.

Examples

Example 1:

type Stage1Value = Stage<number, "Loading">;
type Stage2Value = Stage<string, "Ready">;

const loadingValue: Stage1Value = { value: 123, stage: "Loading" };
const readyValue: Stage2Value = { value: "Data Loaded", stage: "Ready" };

const numberValue: number = getStageValue(loadingValue); // Valid: number
const stringValue: string = getStageValue(readyValue); // Valid: string

// Error: Type 'Stage<number, "Loading">' is not assignable to type 'Stage<string, "Ready">'.
//  Type 'number' is not assignable to type 'string'.
// const invalidAssignment: Stage2Value = loadingValue;

Explanation: Stage1Value represents a number associated with the "Loading" stage, and Stage2Value represents a string associated with the "Ready" stage. The getStageValue function correctly extracts the underlying values. The commented-out line demonstrates the type safety – attempting to assign a Stage1Value to a variable expecting a Stage2Value results in a compile-time error.

Example 2:

type Stage3Value = Stage<boolean, "Processing">;

const processingValue: Stage3Value = { value: true, stage: "Processing" };

// Error: Argument of type 'Stage<boolean, "Processing">' is not assignable to parameter of type 'Stage<number, "Loading">'.
//  Types of property 'value' are incompatible.
//  Type 'boolean' is not assignable to type 'number'.
// const incorrectStage: Stage<number, "Loading"> = processingValue;

Explanation: This example highlights that the StageName is also crucial for type safety. Even if the underlying value type (boolean vs. number) were compatible, the StageName mismatch would prevent the assignment.

Example 3: (Edge Case - Incorrect Stage Name)

type Stage4Value = Stage<string, "Ready">;

const incorrectStageName: Stage<string, "Loading"> = { value: "Incorrect Stage", stage: "Loading" };

// Error: Type 'Stage<string, "Loading">' is not assignable to type 'Stage<string, "Ready">'.
//  String literal types '"Loading"' and '"Ready"' are not assignable to each other.

Explanation: This demonstrates that the StageName must match the expected type. Using "Loading" when "Ready" is expected results in a compile-time error.

Constraints

  • The StageName must be a string literal type.
  • The getStageValue function must return the underlying value T without any runtime checks related to the StageName.
  • The solution must be written in TypeScript.
  • The solution should be as concise and readable as possible.

Notes

  • Consider using conditional types and mapped types to achieve the desired type safety.
  • The Stage type should effectively "tag" the value with the stage name at compile time.
  • The stage property in the example values is purely for demonstration and should not be used in the implementation of the Stage type itself. It's just to illustrate how the values might be structured. The type system should enforce the stage association.
  • Focus on the type-level aspects of this problem; runtime validation is not required.
type Stage<T, StageName extends string> = {
    value: T;
    stage: StageName;
};

function getStageValue<T, StageName extends string>(stage: Stage<T, StageName>): T {
    return stage.value;
}
Loading editor...
typescript