Home > Enterprise >  Typescript changes type of Map value
Typescript changes type of Map value

Time:10-01

I am newbie in TS and need some help. I've got really weird behavior of TS. I try to make a map of arrow functons and then call them by their name. Look at this

const first = (x: string): string => x.toUpperCase();
const second = (x: number): number => x ** 2

type FirstType = typeof first;
type SecondType = typeof second;

type MyUnion = FirstType | SecondType;

const handlers = new Map<string, MyUnion>();
handlers.set('first', first);
handlers.set('second', second);

// here myHandler has this type, as predicted:
// MyUnion | undefined
const myHandler = handlers.get('first');
if (myHandler) {
    // but here myHandler somehow has this type:
    // (x: never) => string | number

    // that's why i got this error
    // Argument of type 'number' is not assignable to parameter of type 'never'.(2345)
    const a = myHandler(10)
    
    // Argument of type 'string' is not assignable to parameter of type 'never'.(2345)
    const b = myHandler('asd')

}

Could enybody explain what's going on here? Why TS has changed type of the value of the map?

CodePudding user response:

That's because your map returns a function whose only parameter is an intersection of all the possible types, and number & string have no overlap, hence the never.

The reason why it's invalid is because it assumes you will call the returned handler with a parameter whose type is valid for all of the registered handlers.

You could eventually do a discriminated union to allow this:

Playground

interface StringHandler {
  type: 'string';
  handler: (x: string) => string;
}

interface NumberHandler {
  type: 'number';
  handler: (x: number) => number;
}

type HandlerUnion = StringHandler | NumberHandler

const handlers = new Map<string, HandlerUnion>();
const first: StringHandler = {
  type: 'string',
  // Notice x is already inferred as a string
  handler: x => x.toUpperCase(),
}
const second: NumberHandler = {
  type: 'number',
  // Notice x is already inferred as a string
  handler: x => x ** 2,
}

handlers.set('first', first);
handlers.set('second', second);

const myHandler = handlers.get('first');
if (myHandler) {
  // This check ensures the handler is a StringHandler thanks to the discriminant "type" property
  if (myHandler.type === 'string') {
    myHandler.handler('asd')
  }
  // This check ensures the handler is a NumberHandler thanks to the discriminant "type" property
  if (myHandler.type === 'number') {
    myHandler.handler(10)
  }
}

CodePudding user response:

The map has nothing to do with it; the issue is with your MyUnion type:

declare const foo: MyUnion;
foo(0);
//  ^
// Argument of type 'number' is not assignable to parameter of type 'never'.

When you union two TypeScript functions, the compiler doesn't know what the actual type of your parameters will be at runtime; without more information, it can only choose a type that satisfies all possibilities. In the case of your union, that type is never since number and string have no overlap.

Consider this example where the two function types do overlap:

interface Pet {
  name: string
}

interface Dog extends Pet {
  breed: string
}

declare function getBreed(d: Dog): string
declare function getPetType(p: Pet): string
declare const fn: typeof getPetType | typeof getBreed;
declare const p: Pet;
declare const d: Dog;

fn(d); // ok
// type hint:
//  const fn: (d: Dog) => string
fn(p);
// ^ 
//  Argument of type 'Pet' is not assignable to parameter of type 'Dog'.

Playground.

Here, Dog was the only type that satisfies both Pet and Dog.

This is related to the concept of contravariance.

  • Related