Type '(s: string) => string' is not assignable to type '<T>(t: T) => T'

Viewed 139

I declared a Typescript interface like so:

interface ProductFieldMapping {
  name: string,
  mapping: (p: Product) => string | number,
  formatting: <T>(t: T) => T,
}

And a function that returns a list of such fields like so:

export const mappingFunctions = (host: string): ProductFieldMapping[] => {
  const mappings = defaultFunctions(host)

  return [
    {
      name: 'title',
      mapping: mappings.productName,
      formatting: (s: string) => s,
    },
  ]
}

But I get this error when I want to assign to formatting this simple function (s: string) => s

Type '(s: string) => string' is not assignable to type '<T>(t: T) => 
T'.

What am I missing?

Possible fix:

type MappingResult = | string | number

interface ProductFieldMapping<T> {
  name: string,
  mapping: (p: Product) => T,
  formatting: (t: T) => T,
}

export const mappingFunctions = (host: string): ProductFieldMapping<MappingResult>[] => {
  const mappings = defaultFunctions(host)

  return [
    {
      name: 'title',
      mapping: mappings.productName,
      formatting: compose([limitSize(255), replaceNewLine(' ')]),
    }]
}
2 Answers

You need to give the type when you invoke the function.
In your case, you should replace string with T.
Because you just assign the function, not invoke it.

formatting: <T>(s: T) => s,

Then while you are invoking the function, then you give a type to your function.

formatting<string>("1") // it doesn't throw error
formatting<number>(1) // it doesn't throw error

EDIT

I saw documentation has a chapter about Generic Classes.
There is same way which VLAZ did, but documentation used classes.

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}
  
let myGenericString = new GenericNumber<string>();
myGenericString.zeroValue = "0";
myGenericString.add = function(x, y) {
    return x + y.toLocaleLowerCase(); // it works
};

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) {
    return Math.round(y) // it works
};

You can tighten up your types by using a discriminated union (also see here) for your possible ProductFieldMappings.

A discriminated union is where you have a union over multiple types but can use a property on them to narrow down the type.

interface ProductFieldMapping<T> {
  name: keyof Product,
  mapping: (p: Product) => T,
  formatting: (t: T) => T,
}

interface StringFieldMapping extends ProductFieldMapping<string> {
    type: "string";
}

interface NumberFieldMapping extends ProductFieldMapping<number> {
    type: "number";
}

type Mapping = StringFieldMapping | NumberFieldMapping;

This makes the ProductFieldMapping a generic interface where the result of mapping and formatting are tied together and must use the same format. Since we only have two formats, we make one type for each - StringFieldMapping and NumberFieldMapping. Each of these has a distinct type and thus Mapping is now a discriminated union.

From here you can produce an array of Mappings and you'll know they work with the correct data:

const numFormat = (n: number) => Math.round(n);
const strFormat = (s: string) => s.toLowerCase();

export const mappingFunctions = (host: string): Mapping[] => {
  const mappings = defaultFunctions(host);

  return [
    {                                   // OK
      name: 'title',
      type: "string",                   // it's a string
      mapping: mappings.productName,    // produces string ✔
      formatting: strFormat,            // consumes string ✔
    },
    {                                   // OK
      name: 'price',
      type: "number",                   // it's a number 
      mapping: mappings.price,          // produces number ✔
      formatting: numFormat,            // consumes number ✔
    },
    {                                   // ERROR
      name: 'a',
      type: "string",                   // it's a string
      mapping: mappings.productName,    // produces string ✔
      formatting: numFormat,            // consumes number ❌
    },
    {                                   // ERROR
      name: 'b',
      type: "number",                   // it's a number
      mapping: mappings.price,          // produces number ✔
      formatting: strFormat,            // consumes string ❌
    },
    {                                   // ERROR
      name: 'c',
      type: "string",                   // it's a string
      mapping: (p: Product) => 42,      // produces number ❌
      formatting: strFormat,            // consumes string ✔
    },
    {                                   // ERROR
      name: 'd',
      type: "number",                   // it's a number
      mapping: (p: Product) => "hello", // produces string ❌
      formatting: numFormat,            // consumes number ✔
    },
  ]
}

Playground Link

Related