Home > Back-end >  Recursively typed function definition in typescript
Recursively typed function definition in typescript

Time:03-08

I have the following type in typescript:

type wrapper<A extends Array<any> = []> = {
  chain<T extends string>(key: T): wrapper<[...A, T]>;
  x: A;
};

Inspired by the following challenge: https://github.com/type-challenges/type-challenges/blob/master/questions/12-medium-chainable-options/README.md

The idea is I would be able to do something like this:

let tmp = wrapper.chain('a').chain('b'); // Expected type wrapper<["a", "b"]>

However I am unable to implement the function definition, since it is meant to return a type of wrapper with a function definition for chain, that also needs to return a function etc... Is it possible to implement this type in javascript with typesafety?

CodePudding user response:

If you want to use functions and plain objects, the easiest thing to do is write a generic function (let's call it w()) that takes a value value of some string[] subtype A and produces a Wrapper<T>:

type Wrapper<A extends string[] = []> = {
    value: A;
    chain<T extends string>(key: T): Wrapper<[...A, T]>;
};

function w<A extends string[]>(value: A): Wrapper<A> {
    return {
        value,
        chain(key) {
            return w([...value, key]);
        }
    }
}

This is seen as type safe, because the compiler understands that a spread like [...value, key] will produce a variadic tuple type like [...A, T] when T is the type of key. Then wrapper is just w([]) where you pass in the empty tuple of type []:

const empty: [] = []
const wrapper: Wrapper = w(empty);
    
const val = wrapper.chain("a").chain("b").chain("c").value;
// const val: ["a", "b", "c"]
console.log(val); // ["a", "b", "c"]

If you don't want to expose w and empty, you can use modules or namespaces where you just export wrapper, or you could write an immediately invoked function expression to get the same effect:

const wrapper: Wrapper = (() => {
    function w<A extends string[]>(value: A): Wrapper<A> {
        return {
            value,
            chain(key) {
                return w([...value, key]);
            }
        }
    }
    const empty: [] = []
    return w(empty)
})();

Or even more simply:

const wrapper: Wrapper = (function w<A extends string[]>(value: A): Wrapper<A> {
    return {
        value,
        chain(key) {
            return w([...value, key]);
        }
    }
})([]);

Also note that if you're not careful, the value [] will be interpreted by the compiler as the type never[] instead of the tuple type []. One way to discourage that is to replace the array type A with a variadic tuple type [...A]; it means basically the same thing, but it gives the compiler a hint that you want a tuple type:

const wrapper = (function w<A extends string[]>(value: [...A]): Wrapper<A> {
    return {
        value,
        chain(key) {
            return w([...value, key]);
        }
    }
})([]);

Playground link to code

CodePudding user response:

This can be achieved with classes and generics. You could also go with using functions and returning objects, but I find it easier with classes. Here's a possible solution:

class Wrapper<Item, Items extends unknown[] = []> {
  items: Items;

  constructor(items?: Items) {
    this.items = items as Items ?? [];
  }

  chain<T extends Item>(item: T): Wrapper<unknown extends Item ? T : T | Item, [...Items, T]> {
    return new Wrapper([...this.items, item]);
  }
}

const wrapper = new Wrapper<string>();

const tmp = wrapper.chain("a").chain("b"); // Wrapper<string, ["a", "b"]>

TypeScript Playground Link

Then you can do the following to get the data within the tuple:

type Inner = Wrapper<string, ["a", "b"]> extends Wrapper<infer Item, infer Values> ? Values : never;

Edit: If you want to use functions, then you can just make a wrapper function that returns new Wrapper<T>().

  • Related