简体   繁体   English

Typescript 匹配 function 类型和参数类型的 map

[英]Typescript match between map of function types and argument types

I have an object mapping functions, and an interface (with the same keys as the object) mapping the argument types for said functions.我有一个 object 映射函数和一个接口(与对象具有相同的键)映射所述函数的参数类型。 Now I'm trying to write a function that can call either one, depending on a key.现在我正在尝试编写一个 function 可以调用任何一个,具体取决于一个键。 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.我不得不把rest as any因为我无法让这些类型工作。 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?有没有办法在这里保持类型安全,或者我的方法在概念上是错误的?

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() .这里的根本问题与编译器无法在blended()的实现中看到func的类型和rest的类型之间的相关性有关。 This situation is essentially the subject of microsoft/TypeScript#30581 , and the recommended approach to deal with it is detailed in microsoft/TypeScript#47109 .这种情况本质上是microsoft/TypeScript#30581的主题,处理它的推荐方法在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 .如果您希望编译器验证funcMap[key]是否接受rest类型的参数,那么您需要将funcMap的类型显式表示为Tags联合上的映射类型,其中每个属性都采用编译器类型的单个参数推断rest If you look inside blended , the type of rest is Omit<BlendedProps<T>, "key" | "z" | "x">如果你看里面blendedrest的类型是Omit<BlendedProps<T>, "key" | "z" | "x"> Omit<BlendedProps<T>, "key" | "z" | "x"> Omit<BlendedProps<T>, "key" | "z" | "x"> . Omit<BlendedProps<T>, "key" | "z" | "x"> So you can annotate funcMap like this:所以你可以像这样注释funcMap

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 ;这并没有真正改变funcMap的类型; indeed the compiler can see that the initializer value of type { oneF: (props: OneFProps) => string; twoF: (props: TwoFProps) => string; }确实编译器可以看到 type { oneF: (props: OneFProps) => string; twoF: (props: TwoFProps) => string; }的初始值设定项值。 { oneF: (props: OneFProps) => string; twoF: (props: TwoFProps) => string; } { 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. { oneF: (props: OneFProps) => string; twoF: (props: TwoFProps) => string; }可分配给{ [T in Tags]: (props: Omit<BlendedProps<T>, "key" | "z" | "x">) => string }类型的变量,因此它们是兼容的类型。

But the difference in representation is important, because now the implementation of blended() type checks:但是表示的差异很重要,因为现在blended()类型检查的实现:

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.实际上,现在funcMap[key]的类型被推断为(props: Omit<BlendedProps<T>, "key" | "z" | "x">) => string ,并且该参数类型与类型相同的rest ,因此可以根据需要接受funcMap[key](rest)

Playground link to code Playground 代码链接

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"}

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.你需要判断 props 到底是什么类型,但是太复杂了,而且还有运行时效果。

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' }))

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM