I hope you are doing fine.
I was surprised by some code that doesn’t work anymore with typescript:
Here is my minimal reproduction scenario:
type Value<T> = { t: T } type Test<T, E = Value<T>> = { value: E } function constructValue<T>(value: T): Value<T> { return {t: value} } function constructTest<T, E = Value<T>>( value: T, sl?: (e: T) => E): Test<T, E> { return { // ts warning: 'E' could be instantiated with an arbitrary type which could be unrelated to 'Value<T> | E'. // IDE says value is of type 'E = value<T>' value: typeof sl === "function" ? sl(value) : constructValue(value) } } // if we assign then destructure later, it s fine const result = constructTest(5) const {value: {t: t1}} = result; // t: number // --> destructuring directly make it try to create the optional parameter // rather than using the default one const {value: {t: t2}} = constructTest(5); // t: any // if we assign then destructure later, it s fine const {value} = constructTest({hello: "world"}); // value: Value<{hello: string}> const {t: {hello}} = value; // t: {hello: string} // --> destructuring directly make it try to create the optional parameter // rather than using the default one const {value: {t: t3}} = constructTest({hello: "world"}); // t: any // adding the selector that syncs the optional generic type seems to work as expected const {value: {override: o1}} = constructTest(5, e => ({override: e})); // override: number const {value: {override: o2}} = constructTest(5, e => ({override: e.toString()})); // override: string
The goal is to create a generic function with two types, when the second corresponds to a selected value by an optional parameter.
function getResults<T, E = State<T>>(payload: T, selector?: (e: T) => E): Output<T, E> { // if selector present return it, or create a state<T> }
The issue is that it is like when we destructure the variables when calling the function, it tries to invent a generic type E
of whatever im destructuring, rather than using the default generic type (which was the case for me some weeks ago).
// if we assign then destructure later, it s fine const result = constructTest(5) const {value: {t: t1}} = result; // t: number // --> destructuring directly make it try to create the optional parameter // rather than using the default one const {value: {t: t2}} = constructTest(5); // t: any
I cannot really understand what’s going wrong in here. Any help is much appreciated.
Here is a sandbox copy of the previous code.
Best regards.
Advertisement
Answer
There was indeed something wrong, as the original declaration suggests that it can be used like
constructTest<number, "something unrelated">(3) //(E is provided, but sl is not)
In this case the return value would be {value: {t: 3}}
which is incompatible with Test<T, E>
= {value: "something unrelated" }
.
One workaround is by function overloading.
type Value<T> = { t: T } type Test<T, E = Value<T>> = { value: E } function constructValue<T>(value: T): Value<T> { return {t: value} } function constructTest<T>(value: T): Test<T>; function constructTest<T, E>(value: T, sl: (e: T) => E): Test<T, E>; function constructTest<T, E>(value: T, sl?: (e: T) => E): Test<T, E | Value<T>> { return { value: typeof sl === "function" ? sl(value) : constructValue(value) } } const result = constructTest(5) const { value: { t: t1 } } = result; // t1: number const { value: { t: t2 } } = constructTest(5); // t2: number, no longer just any const { value } = constructTest({ hello: "world" }); const { t: { hello } } = value; // hello: string const { value: { t: t3 } } = constructTest({ hello: "world" }); // t3: {hello: string}, not just any const { value: { override: o1 } } = constructTest(5, e => ({ override: e })); // o1: number const { value: { override: o2 } } = constructTest(5, e => ({ override: e.toString() })); // o2: string
One down side is that the developer will be responsible for narrowing down the return type of the overload signatures correctly (like : Test<T>
and : Test<T, E>
), as Typescript doesn’t seem to check overloaded signatures against the body directly (even though it checks that it can be a subtype of the implementation signature).