Home > Net >  Typescript match between map of function types and argument types
Typescript match between map of function types and argument types

Time:07-30

I have an object mapping functions, and an interface (with the same keys as the object) mapping the argument types for said functions. Now I'm trying to write a function that can call either one, depending on a key. Here's a simplified version of the problem:

interface OneFProps {
  a: string;
  b: string;
}

const oneF = (props: OneFProps) => {
  const { a, b } = props;
  return `oneF ${a} ${b}`;
};

interface TwoFProps {
  c: string;
  d: string;
}

const twoF = (props: TwoFProps) => {
  const { c, d } = props;
  return `oneF ${c} ${d}`;
};

const funcMap = {
  oneF: oneF,
  twoF: twoF,
};

interface typeMap {
  oneF: OneFProps;
  twoF: TwoFProps;
}

type tags = keyof typeMap;

type BlendedProps<T extends tags> = {
  key: T;
  z?: string;
  x?: string;
} & typeMap[T];

const blended = <T extends tags>(props: BlendedProps<T>) => {
  const { key, z, x, ...rest } = props;
  const func = funcMap[key];

  return func(rest as any);
};

console.log(blended<'oneF'>({ key: 'oneF', a: 'AAA', b: 'bbb' }));
console.log(blended<'twoF'>({ key: 'twoF', c: 'ccc', d: 'DDD' }));

I had to put that rest as any because I couldn't get the types to work. I did some research and found stuff about distributive conditional types, but I don't know if that's really the problem here and couldn't come up with a solution for that. Is there a way to preserve type safety here or is my approach conceptually wrong?

CodePudding user response:

The underlying issue here has to do with the compiler being unable to see the correlation between the type of func and the type of rest inside the implementation of blended(). This situation is essentially the subject of microsoft/TypeScript#30581, and the recommended approach to deal with it is detailed in microsoft/TypeScript#47109.

If you want the compiler to verify that funcMap[key] accepts a parameter of type rest, then you need to express the type of funcMap explicitly as a mapped type over the Tags union, where each property takes a single argument of the type the compiler infers for rest. If you look inside blended, the type of rest is Omit<BlendedProps<T>, "key" | "z" | "x">. So you can annotate funcMap like this:

const funcMap: { [T in Tags]: 
  (props: Omit<BlendedProps<T>, "key" | "z" | "x">) => string 
} = {
    oneF: oneF,
    twoF: twoF,
};

This doesn't really change the type of funcMap; indeed the compiler can see that the initializer value of type { oneF: (props: OneFProps) => string; twoF: (props: TwoFProps) => string; } is assignable to the variable of type { [T in Tags]: (props: Omit<BlendedProps<T>, "key" | "z" | "x">) => string }, so they are compatible types.

But the difference in representation is important, because now the implementation of blended() type checks:

const blended = <T extends Tags>(props: BlendedProps<T>) => {
    const { key, z, x, ...rest } = props;
    const func = funcMap[key];
    return func(rest); // okay
};

Indeed, now the type of funcMap[key] is inferred to be (props: Omit<BlendedProps<T>, "key" | "z" | "x">) => string, and that parameter type is identical to the type of rest, so funcMap[key](rest) is accepted, as desired.

Playground link to code

CodePudding user response:

Hope it helps you

class Props<K extends string, P extends {}> {
  key: K;
  data: P;

  constructor(data: { key: K } & P) {
    this.key = data.key;
    this.data = data;
  }

  toString() {
    return JSON.stringify(this.data);
  }
}

const oneF = new Props({ key: 'oneF', a: 'AAA', b: 'bbb' });
console.log(oneF.toString());
// {"key":"oneF","a":"AAA","b":"bbb"}

const twoF = new Props({ key: 'twoF', c: 'ccc', d: 'DDD' });
console.log(twoF.toString());
// {"key":"twoF","c":"ccc","d":"DDD"}

CodePudding user response:

const blended2 = <T extends tags>(props: BlendedProps<T>) => {
  const { key, z, x, ...rest } = props
  const func = funcMap[key]
  func()
  // const func: (props: OneFProps & TwoFProps) => string

  // why the props is an intersection type ? 
  // since function params are contravariance (you can search this to find more ) , 
  // in type system , ts can't know what props type is in real runtime
  // to make func type safe , only the type of params is the intersection of OneFProps & TwoFProps.

  return func(rest as any)
  
}

how to fix this ?

you need to judge what type props exactly is,but it is too complex,and it also has runtime effect.

interface OneFProps {
  a: string
  b: string
}

const oneF = (props: OneFProps) => {
  const { a, b } = props
  return `oneF ${a} ${b}`
}

interface TwoFProps {
  c: string
  d: string
}

const twoF = (props: TwoFProps) => {
  const { c, d } = props
  return `oneF ${c} ${d}`
}

const funcMap = {
  oneF: oneF,
  twoF: twoF,
}

interface typeMap {
  oneF: OneFProps
  twoF: TwoFProps
}

type tags = keyof typeMap

type BlendedProps<T extends tags> = {
  key: T
  z?: string
  x?: string
} & typeMap[T]


const judgeProps = <U extends tags>(
  props: { key: unknown },
  target: U
): props is BlendedProps<U> => {
  return props.key === target
}

const blended = <T extends tags>(props: BlendedProps<T>) => {
  if (judgeProps(props, 'oneF')) {
    const { key, z, x, ...rest } = props
    const func = funcMap[key]
    return func(rest)
  } else if (judgeProps(props, 'twoF')) {
    const { key, z, x, ...rest } = props
    const func = funcMap[key]
    return func(rest)
  }
}

console.log(blended({ key: 'oneF', a: 'AAA', b: 'bbb' }))
console.log(blended({ key: 'twoF', c: 'ccc', d: 'DDD' }))

  • Related