Skip to content
Advertisement

Weird typescript behavior when destructuring and having optional generic types

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).

User contributions licensed under: CC BY-SA
1 People found this is helpful
Advertisement