Typescript 根据参数定义函数参数和返回值。

huangapple go评论64阅读模式
英文:

Typescript define function args and return value based on arg

问题

I've got a function that translates data two ways. One of the args will determine the direction of your translation and the other arg and return value will depend on this.

type DataTranslation =   
    | ((arg: { toType: "number"; data: string }) => number | undefined)
    | ((arg: { toType: "string"; data: number }) => string | undefined);

const dataTranslation: DataTranslation = ({toType, data}) => {
    if(toType === 'number') {
        return Number.parseInt(data);
    }
    if(toType === 'string') {
        return `${data}`;
    }
}

Effectively this function is one of two things. If toType is "number" than data will be string and return value will be number (and vice versa). The error I get though is this:

Type '({ toType, data }: { toType: any; data: any; }) => string | number | undefined' is not assignable to type 'DataTranslation'.
  Type '({ toType, data }: { toType: any; data: any; }) => string | number | undefined' is not assignable to type '(arg: { toType: "number"; data: string; }) => number | undefined'.
    Type 'string | number | undefined' is not assignable to type 'number | undefined'.
      Type 'string' is not assignable to type 'number | undefined'.

It appears TypeScript is merging my return values. Also, the error appears to not apply my type properly, I can see { toType: any; data: any; } in there.

英文:

I've got a function that translates data two ways. One of the args will determine the direction of your translation and the other arg and return value will depend on this.

type DataTranslation =   
    | ((arg: { toType: "number"; data: string }) => number | undefined)
    | ((arg: { toType: "string"; data: number }) => string | undefined);

const dataTranslation: DataTranslation = ({toType, data}) => {
    if(toType === 'number') {
        return Number.parseInt(data);
    }
    if(toType === 'string') {
        return `${data}`;
    }
}

Effectively this function is one of two things. If toType is "number" than data will be string and return value will be number (and vice versa). The error I get though is this:

Type '({ toType, data }: { toType: any; data: any; }) => string | number | undefined' is not assignable to type 'DataTranslation'.
  Type '({ toType, data }: { toType: any; data: any; }) => string | number | undefined' is not assignable to type '(arg: { toType: "number"; data: string; }) => number | undefined'.
    Type 'string | number | undefined' is not assignable to type 'number | undefined'.
      Type 'string' is not assignable to type 'number | undefined'.

It appears typescript is merging my return values. Also, the error appears to not apply my type properly, I can see { toType: any; data: any; } in there.

答案1

得分: 1

以下是您要翻译的内容:

你的问题的核心在于使返回类型依赖于输入类型。 TypeScript GitHub 存储库上有一个名为“dependent-type-like functions”的请求,但可以使用各种方法来解决它;确保安全性很困难。

以下代码正确类型化了定义和调用站点的参数,以及调用站点的返回类型,但不允许在函数内返回不正确类型的值。

/**
 * This is a mapping of names to types.
 */
type DataNameMap = {
  string: string;
  number: number;
};

/**
 * This is a mapping of output type names to the corresponding input type.
 * (It's that way round because the input is keyed by the output type name.)
 */
type DataTranslationMap = {
  string: DataNameMap["number"];
  number: DataNameMap["string"];
};

/**
 * This converts the names and translation mappings to a combined object
 * so that TypeScript can see how the input and output types are related
 * when we define the function.
 * For every name in the first mapping, it defines an object with the
 * argument type and the output type.
 */
type DataTranslationObject = {
  [key in keyof DataNameMap]: {
    arg: {
      to: key;
      data: DataTranslationMap[key];
    };
    res: DataNameMap[key];
  };
};

/**
 * This is a union of each property of `DataTranslationObject`.
 * */
type DataTranslationUnion = DataTranslationObject[keyof DataTranslationObject];

/**
 * This is the function itself.
 *
 * Details in the rest of the answer.
 */

// 1. `to` property
function dataTranslation<Name extends keyof DataTranslationObject>(arg: {
  to: Name;
  data: DataTranslationObject[Name]["arg"]["data"];
}): DataTranslationObject[Name]["res"] | undefined;

// 2. Whole arg
// function dataTranslation<Arg extends DataTranslationUnion["arg"]>(
//   arg: Arg
// ): Extract<DataTranslationUnion, { arg: Arg }>["res"] | undefined;

// Implementation
function dataTranslation({
  to,
  data,
}: DataTranslationUnion["arg"]): DataTranslationUnion["res"] | undefined {
  if (to === "number") {
    return Number.parseInt(data);
  }

  if (to === "string") {
    return data.toString();
  }

  return undefined;
}

因此,这是函数本身的类型化。

重载提供了输入和输出之间的调用站点关联。这里有两个选项:

  1. 类型参数 Name 是“翻译对象”的键。 它从输入的 to 属性中推断出来。 "翻译对象" 用它来确定输入的 data 属性类型和返回类型。 如果有时只想允许特定类型,可以通过设置类型参数来实现。

  2. 类型参数 Arg 是所有可能的 arg 属性的联合。 它从整个输入中推断出来。 返回类型是通过从“翻译联合”中提取包含该类型的 arg 属性的成员来确定的。 在复杂情况下,这可能有点难以理解,但不需要再次编写参数类型。

您可以同时使用两者,但我认为与仅使用第一选项相比没有好处。

实现签名使用“翻译联合”。这使得 TypeScript 可以在函数内部关联 todata 属性(即使它们被解构,如下所示)。 由于类型系统的限制,我们不能向 TypeScript 显示返回类型与输入类型相关联,因此确保返回的值是正确的类型!(至少在这种方法中是如此。)

TS Playground

相关:@jcalz 称之为“相关联联合”的问题。相关的 GitHub 问题和 PR:

另请参阅:@jcalz 对“TypeScript 泛型在调用时有效,但在函数体内无效?”的回答,其中提供了有关在通用类型参数中定义参数以及解决方法的信息。在这里也适用(至少在某种程度上)。

英文:

The core of your problem is making the return type depend on the input type. There is a request for proper support for this on the TypeScript GitHub repo, under the name of "dependent-type-like functions". However, it can be worked around using various methods; guaranteeing safety is hard, though.

The following code types the definition and call-site arguments correctly, and the call-site return type correctly, but does not disallow returning a value of an incorrect type within the function.

/**
 * This is a mapping of names to types.
 */
type DataNameMap = {
  string: string;
  number: number;
};

/**
 * This is a mapping of output type names to the corresponding input type.
 * (It&#39;s that way round because the input is keyed by the output type name.)
 */
type DataTranslationMap = {
  string: DataNameMap[&quot;number&quot;];
  number: DataNameMap[&quot;string&quot;];
};

/**
 * This converts the names and translation mappings to a combined object
 * so that TypeScript can see how the input and output types are related
 * when we define the function.
 * For every name in the first mapping, it defines an object with the
 * argument type and the output type.
 */
type DataTranslationObject = {
  [key in keyof DataNameMap]: {
    arg: {
      to: key;
      data: DataTranslationMap[key];
    };
    res: DataNameMap[key];
  };
};

/**
 * This is a union of each property of `DataTranslationObject`.
 * */
type DataTranslationUnion = DataTranslationObject[keyof DataTranslationObject];

/**
 * This is the function itself.
 *
 * Details in the rest of the answer.
 */

// 1. `to` property
function dataTranslation&lt;Name extends keyof DataTranslationObject&gt;(arg: {
  to: Name;
  data: DataTranslationObject[Name][&quot;arg&quot;][&quot;data&quot;];
}): DataTranslationObject[Name][&quot;res&quot;] | undefined;

// 2. Whole arg
// function dataTranslation&lt;Arg extends DataTranslationUnion[&quot;arg&quot;]&gt;(
//   arg: Arg
// ): Extract&lt;DataTranslationUnion, { arg: Arg }&gt;[&quot;res&quot;] | undefined;

// Implementation
function dataTranslation({
  to,
  data,
}: DataTranslationUnion[&quot;arg&quot;]): DataTranslationUnion[&quot;res&quot;] | undefined {
  if (to === &quot;number&quot;) {
    return Number.parseInt(data);
  }

  if (to === &quot;string&quot;) {
    return data.toString();
  }

  return undefined;
}

So, typing of the function itself.

The overload provides the call-site correlation between the input and output.
There are two options here:

  1. The type parameter, Name, is a key of the "translation object".
    It is inferred from the to property of the input.
    The "translation object" is indexed with it to determine
    the input's data property type and the return type.
    This is useful if you sometimes want to only allow specific
    types, which can be done by setting the type parameter.

  2. The type parameter, Arg, is a union of all the possible arg properties.
    It is inferred from the entire input.
    The return type is determined by extracting from the "translation union"
    the member that contains an arg property of that type.
    This may be a little harder to reason about in complex cases,
    but doesn't require you to write out the arg type again.

You can use both at once, but I see no benefits in that over simply using the first option.

The implementation signature uses the "translation union".
This allows TypeScript to relate the to and data properties within
the function (even if they were destructured, as below).
Because of limitations in the type system, we can't show TypeScript that
the return type is related to the input type, so make sure the returned
values are the correct type!
(At least with this approach.)

TS Playground

Related: what @jcalz has been calling "correlated unions". The relevant GitHub issue and PR:

See also: @jcalz's answer to "TypeScript generics work when you call it but not in the function body?" for info and workarounds for the general problem of defining parameters using generic type parameters. Applies here (to some extent, at least).

huangapple
  • 本文由 发表于 2023年5月25日 00:36:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/76325719.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定