Home > OS >  Why can't Data, which extends {}, be assigned to {}? Does {} != {}?
Why can't Data, which extends {}, be assigned to {}? Does {} != {}?

Time:12-02

I don't understand why this code errors. Could someone help me? I have looked all around stack overflow and found a lot of way too specific examples of this, but I don't understand the most basic question. Why can't Data, which extends {}, be assigned to {}? Does {} != {}?

function fn<Data extends {}>(data: Data = {}) { }
//                          ^^^^^^^^^^^^^^^^^
//                    Type '{}' is not assignable to type 'Data'.

Type '{}' is not assignable to type 'Data'. '{}' is assignable to the constraint of type 'Data', but 'Data' could be instantiated with a different subtype of constraint '{}'.ts(2322)

CodePudding user response:

Because the type of Data could be something that requires properties not present in {}.

Say, for example, you create the following interface:

interface FooData {
  foo: string
}

We know we cannot do this:

const data: FooData = {};
//                    ^^ - missing required property `foo: string`

Yet that's exactly what you are attempting in your fn declaration when you set the default value to {}. In TypeScript's words:

'{}' is assignable to the constraint of type 'Data', but 'Data' could be instantiated with a different subtype of constraint '{}'.

In this case, FooData is the "different subtype of constraint '{}'" that requires property foo, which is not present on {}. {} does not guarantee that the type parameter passed to Data will be satisfied by {}, so {} is not allowed as a default.


Also, you probably meant to extend type Record<string, any> instead. {}, the empty type, is often not what you want (see here) as it will allow everything except for null and undefined.

Note: in most cases, it is not feasible to define a default value for a generically typed parameter since the actual type can be more specific than the default value (as explained above).

CodePudding user response:

After playing around with what the other people on here have said, I came up with this:

1st try

// `Data extends {}` allows unexpected types to be passed in.
function fn1<Data extends {}> ( data: Data = {} ) { }
//                              ^^^^^^^^^^^^^^^
//         Type '{}' is not assignable to type 'Data'.

fn1() // allowed as expected
fn1( undefined ) // allowed as expected
fn1( { foo: 'bar' } ) // allowed as expected
fn1( [ 1, 2, 3 ] ) // allowed as expected

fn1( 'baz' ) // allowed NOT as I expected
fn1( 42 ) // allowed NOT as I expected

fn1( null ) // ts error as expected

2nd try

// `Data extends Record<string, any>` allows only objects to be passed in.
function fn2<Data extends Record<string, any>> ( data: Data = {} ) { }
//                                               ^^^^^^^^^^^^^^^
//                          Type '{}' is not assignable to type 'Data'.

fn2() // allowed as expected
fn2( undefined ) // allowed as expected
fn2( { foo: 'bar' } ) // allowed as expected
fn2( [ 1, 2, 3 ] ) // allowed as expected

fn2( 'baz' ) // ts error as expected
fn2( 42 ) // ts error as expected
fn2( null ) // ts error as expected

3rd try

// casting {} as Data works as expected
function fn3<Data extends Record<string, any>> ( data: Data = {} as Data ) { }

fn3() // allowed as expected
fn3( undefined ) // allowed as expected
fn3( { foo: 'bar' } ) // allowed as expected
fn3( [ 1, 2, 3 ] ) // allowed as expected

fn3( 'baz' ) // ts error as expected
fn3( 42 ) // ts error as expected
fn3( null ) // ts error as expected
  • Related