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