Search code examples
interfacecallbackargumentsextendtypescript-generics

Typescript generic callback with arguments that extend an interface, TS2345


How can I create a callback that takes arguments, where the arguments can be anything that extends some interface, but apart from that I don't know the exact type ?

e.g.

const fnct = function( data: DataAny, callback: GenericCallback<DataAny> ): void {
    callback( data );
};
// DataAny here should be like "argument type that can be anything but has to extend DataAny"

Example

To be more specific, how can I avoid the typescript errors in the last two lines here
(maybe easiest to read from bottom up):


// -- unchangeable interface, comes from external library --
interface DataAny {
    type: string;
    [more: string]: any;
}

// -- my interfaces / types --
interface DataBook extends DataAny { pages: number }
interface DataWater extends DataAny { liters: number }

type GetQuantity<D> = ( data: D ) => void; // callback function type

// -- generic function --
const genericQuantityInfo = function( data: DataAny, f: GetQuantity<DataAny> ): void {
    console.log('quantity: ', f( data ));
};

// -- data --
const book: DataBook  = { type: 'book',  pages: 303 };
const water: DataWater = { type: 'water', liters: 12 };

// -- callbacks --
const getQuantityBook: GetQuantity<DataBook>  = function( data: DataBook ){ return data.pages + ' pages'; };
const getQuantityWater: GetQuantity<DataWater> = function( data: DataWater ){ return data.liters + ' liters'; };

// -- calls --
genericQuantityInfo( book,  getQuantityBook ); // <-- error TS2345: 'GetQuantity<DataBook>' is not assignable to 'GetQuantity<DataAny>'. 'pages' is missing in 'DataAny' but required in 'DataBook'
genericQuantityInfo( water, getQuantityWater ); // <-- error (similar to above)

Not wanted

Note that I can not pass the specific types, like genericQuantityInfo<DataBook> here (this works):

const genericQuantityInfo = function<T>( data: T, f: GetQuantity<T> ): void { console.log('quantity: ', f( data )); };
genericQuantityInfo<DataBook>( book,  getQuantityBook );
genericQuantityInfo<DataWater>( water, getQuantityWater );

The reason why I can't do this is that my use case is actually more complex, using a creator function, and I don't want to pass all possible types <DataBook | DataWater | ... (also I couldn't get this to work as well). like:

const creator = function<T>(){
    return function( data: T, f: GetQuantity<T> ): void {
        console.log('quantity: ', f( data ));
    };
};
creator<DataBook | DataWater /* | ... */ >()(book, getQuantityBook); // <-- still error anyway

But as I simplified a lot here, any Ideas on where I misunderstood something are also welcome. Thanks.


Solution

  • I think I got it:

    Instead of typing as DataAny try to type it as T extends DataAny it means anything that has DataAny props.

    interface DataAny {
        type: string;
        [more: string]: any;
    }
    
    // -- my interfaces / types --
    interface DataBook extends DataAny { pages: number }
    interface DataWater extends DataAny { liters: number }
    
    type GetQuantity<D extends DataAny> = ( data: D ) => void; // callback function type
    
    // -- generic function --
    const genericQuantityInfo = function<T extends DataAny>( data: T, f: GetQuantity<T> ): void {
        console.log('quantity: ', f( data ));
    };
    
    // -- data --
    const book: DataBook  = { type: 'book',  pages: 303 };
    const water: DataWater = { type: 'water', liters: 12 };
    
    // -- callbacks --
    const getQuantityBook: GetQuantity<DataBook>  = function( data: DataBook ){ return data.pages + ' pages'; };
    const getQuantityWater: GetQuantity<DataWater> = function( data: DataWater ){ return data.liters + ' liters'; };
    
    // -- calls --
    genericQuantityInfo( book,  getQuantityBook ); // OK
    genericQuantityInfo( water, getQuantityWater ); // OK
    

    playground