Home > Back-end >  Infinite currying with typescript
Infinite currying with typescript

Time:01-31

I made following code implementing joining of path by infinite currying.

function joinPath(): string
function joinPath(a: string): typeof joinPath
function joinPath(a?: string): string | typeof joinPath {
  if (a === undefined) { return '' }
  const func = (b?: string) => {
    if (b === undefined) { return a }
    return joinPath(`${a}/${b}`)
  }

  return func as typeof joinPath
}


console.log(joinPath('www.github.com')()) // www.github.com
console.log(joinPath('www.github.com')('react')) // www.github.com/react

The result of joinPath is not literal type but string type.

e.g. joinPath('facebook')(): string // not 'facebook'

How can I make the result literal type? It seems I have to use generic type, but I could not find the way to work.

I tried to make the result type as literal using generic type but it did not work.

CodePudding user response:

In order to keep track of the literal type of the string being built up, you're going to need the type of joinPath to itself be generic in that type. And since calling the resulting function with another string is going to have the effect of modifying that string, you will want the type of joinPath to be a recursive generic type. It's easiest to do that with a named interface like:

interface JoinPath<T extends string> {
  (): T;
  <U extends string>(a: U): JoinPath<`${T}/${U}`>
}

So JoinPath<T> is holding onto the string literal type T. If you call it with no arguments you get T as a result. Otherwise, if you call it with an argument of generic type U constrained to string, you get JoinPath<`${T}/${U}`> as a result, where we've used a template literal type to represent the concatenation of T with U.

And now you can annotate the overload call signatures as follows:

function joinPath(): ""
function joinPath<U extends string>(a: U): JoinPath<U>
function joinPath(a?: string) {
  if (a === undefined) { return '' }
  const func = (b?: string) => {
    if (b === undefined) { return a }
    return joinPath(`${a}/${b}`)
  }
  return func
}

So joinPath() returns the empty string "", otherwise joinPath(a)returnsJoinPath`.


Let's test it out:

const w = joinPath();
// const w: ""
console.log(w); // ""

const f = joinPath('www.github.com');
// const f: JoinPath<"www.github.com">

const x = f();
// const x: "www.github.com"
console.log(x); // www.github.com

const g = f("react");
// const g: JoinPath<"www.github.com/react">

const y = g();
// const y: "www.github.com/react"
console.log(y); //  "www.github.com/react" 

const h = g("foobar");
// const h: JoinPath<"www.github.com/react/foobar">
const z = h();
// const z: "www.github.com/react/foobar"
console.log(z); // "www.github.com/react/foobar" 

Looks good. In all situations the type of the variables and the values of the variables agree.

Playground link to code

  • Related