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]);
}
}
})([]);
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"]>
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>()
.