Skip to content

Pipe Operator instead of conditions and deep extendsΒ #61819

Open
@MasterTuto

Description

@MasterTuto

πŸ” Search Terms

  • switch case
  • switch case type level
  • pipe operator ts
  • pipe operator typescript
  • avoid nested extends
  • pipe ts type level
  • type level switch
  • switch typescript

βœ… Viability Checklist

⭐ Suggestion

My suggestion is to add a kind of idiomatic switch case for typescript types, where we could verify a list of extends without nesting.

ERRATUM: It does more than just adding syntax sugar to syntax, and behaves differently to a switch case. In this case, it determines a set of conditions that computes and returns the first found matching conditions. This way we could have more complex checking, instead of a simple "If Type T matches other Type U".

The example below does this. It not only checks "equality" and also does more things like calling other utility changes and then doing a condition check.

So, instead of a "switch case", it would likely feel closer to a syntax sugar to a sequence of "ifs".

Imagine a "replace" function that takes a text, find placeholders and provide an object with that placeholders as keys. This is how it can be achieved now:

type Whitespace = '\n' | '\t' | ' ';

type TrimLeft<T extends string> =
  T extends `${Whitespace}${infer S extends string}` ? TrimLeft<S> : T;
type TrimRight<T extends string> =
  T extends `${infer S extends string}${Whitespace}` ? TrimRight<S> : T;
type Trim<T extends string> = TrimRight<TrimLeft<T>>;

type Equal<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;

type IsJustString<T> = Equal<T, string>;

type Placeholders<T extends string, Result extends string = never> =
  IsJustString<T> extends true
    ? string
    : Equal<T, ''> extends true
      ? Result
      : T extends `${string}{{${infer P extends string}}}${infer Rest extends string}`
        ? Placeholders<Rest, Result | Trim<P>>
        : Result;

export function withText<T extends string>(text: T) {
  return {
    replace: (contents: Record<Placeholders<T>, string>) => {
      let result: string = text;
      for (const [key, value] of Object.entries(contents)) {
        result = result.replace(`{{${key}}}`, value as string);
      }
      return result;
    },
  };
}

const helloWorld = withText("{{greeting}}, {{person}}").replace({
  greeting: "Hello",
  person: "World"
})

This is how it would look on my proposal:

type Whitespace = '\n' | '\t' | ' ';

type TrimLeft<T extends string> =
  |> T extends `${Whitespace}${infer S extends string}`: TrimLeft<S>,
  |> T;
type TrimRight<T extends string> =
  |> T extends `${infer S extends string}${Whitespace}`: TrimRight<S>,
  |> T;
type Trim<T extends string> = TrimRight<TrimLeft<T>>;

type Equal<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;

type IsJustString<T> = Equal<T, string>;

type Placeholders<T extends string, Result extends string = never> =
  |> IsJustString<T> extends true: string,
  |> Equal<T, ''> extends true: Result,
  |> T extends `${string}{{${infer P extends string}}}${infer Rest extends string}`: Placeholders<Rest, Result | Trim<P>>,
  |> Result;

export function withText<T extends string>(text: T) {
  return {
    replace: (contents: Record<Placeholders<T>, string>) => {
      let result: string = text;
      for (const [key, value] of Object.entries(contents)) {
        result = result.replace(`{{${key}}}`, value as string);
      }
      return result;
    },
  };
}

const helloWorld = withText("{{greeting}}, {{person}}").replace({
  greeting: "Hello",
  person: "World"
})

πŸ“ƒ Motivating Example

Sometimes nested extends can be too complicated to understand:

  • The depth is is unmanageable
  • Doesn't separate different concerns (are we still verifying possible values of T or are we doing something else?)
  • Makes it hard to format
  • Makes it hard to read

πŸ’» Use Cases

  1. What do you want to use this for?

Replace deep extends checks

  1. What shortcomings exist with current approaches?
  • Split into different types with different concers
  • Format so that it looks like a "switch case"
  1. What workarounds are you using in the meantime?

I have presented above

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions