Typescript: Why is it that we cannot assign default value to generic type?

Viewed 925

Take the below for instance. I'm not quite sure what the error message means, but it seems that logically, the signature is completely valid. Is this just not supported by TS?

function _createNominalCollection<isOutputOrdered_T extends boolean>(
  input: nominal_T,
  processingFunc: (count: number) => number,
  orderedOutput: isOutputOrdered_T = true,
)

^^^
Type 'boolean' is not assignable to type 'isOutputOrdered_T'.
  'boolean' is assignable to the constraint of type 'isOutputOrdered_T', but 'isOutputOrdered_T' could be instantiated with a different subtype of constraint 'boolean'.ts(2322)
4 Answers

As @VLAZ pointed out _createNominalCollection<false>() is problematic. Let's look at this error again:

'boolean' is assignable to the constraint of type 'isOutputOrdered_T', but 'isOutputOrdered_T' could be instantiated with a different subtype of constraint 'boolean'.ts(2322)

What that means is that you pass an explicit <false> type as the generic parameter, isOutputOrdered_T is now constrained to false but then the default argument is true, which would violate that.

Or to put it another way, true and false are subtypes of boolean, and your function allows the boolean to be constrained to one of those subtypes, but doesn't guarantee assignments to that variable will all be of that same subtype.

Let me propose an alternative.


When you have a function that returns different types based on different arguments, you should always consider if function overloads are better suited to model that instead of generics. They allow you to specifically map argument patterns to specific return type in a simple way without any generics at all.

For example:

// sorted version
function myFn(orderedOutput?: true): 'sorted'

// unsorted version
function myFn(orderedOutput: false): 'unsorted'

// Implementation
function myFn(orderedOutput: boolean = true): 'sorted' | 'unsorted' {
  return orderedOutput ? 'sorted' : 'unsorted'
}

// Testing
const a = myFn(true) // sorted
const b = myFn(false) // unsorted
const c = myFn() // sorted

Here you create tow signatures for your function. The first "sorted" version accepts no argument or true. The second "unsorted" version accepts false. Then you have an implementation that handle both cases.

Playground

The reason you get this error, is because isOutputOrdered_T could be a boolean subtype like type truthy = true or type falsy = false instead of being a complete boolean like type bool = true | false. For example:

function _createNominalCollection<T extends boolean>(
  orderedOutput: T
) {}

type booleanSubtype = true;

_createNominalCollection<booleanSubtype>(false);

In this example the compiler would complain about passing false, because booleanSubtype only allows true as input:

Argument of type 'false' is not assignable to parameter of type 'true'

In case of a truthy boolean subtype, like in the example, your default value wouldn't be a valid input and thats why the compiler is warning you. Your example could work without compiler warning if you typecast your default value like this:

function _createNominalCollection<T extends boolean>(
  orderedOutput: T = false as T
) {}

But as shown before, this wouldn't be actually correct with a truthy subtype, just getting rid of the error.

You have option to not assign default to false - leave it as undefined.

function f<T extends boolean=false>(a?: T) {
   return a ? 'isTrue' : 'isNotTrue'
}

Thought I'd question why do you need this generic in first place. Does return type change depending on this flag? If it does change why does it need to be single function, would it not make more sense to have 2 distinctly named functions?

You get the error because you assign true to orderedOutput as a default parameter.

If you would call _createNominalCollection(..,..,false) the constrainted type parameter isOutputOrdered_T would be inferred to false. Of course it is not allowed to assign the value true to a parameter of type false.

Related