是否可以根据参数更改 TypeScript 中的必需或可选类型?

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

Is it possible to change typescript required or optional depending on a parameter?

问题

I'm trying to use the same function to query in a couple of different ways, and I would like to use typescript to enforce some parameters depending on what parameters they provide to the function.

E.g. If I pass a query, I also need you to pass these other params, but you don't have to pass the query.

Right now, I can check to see if they are passing all required parameters, but I would like to be able to enforce them as required if query is passed to the function

Example function
I would like query to be optional
but if query is passed, I want column and row to be required

// TypeScript

interface GoogleSheetsConfig {
    sheetId: string;
    query?: string;
    column?: string;
    row?: string;
}

export const fetchGoogleSheet = async ({
    sheetId,
    query,
    column,
    row,
}: GoogleSheetsConfig) => {

// if query exists, include column and row
// else ignore query
}

I found this answer that says it does what I'm trying to accomplish, but there is no explanation:
Stack Overflow Link

英文:

I'm trying to use the same function to query in a couple of different ways, and I would like to use typescript to enforce some parameters depending on what parameters they provide to the function.

E.g. If I pass a query, I also need you to pass these other params, but you don't have to pass the query.

Right now, I can check to see if they are passing all required parameters, but I would like to be able to enforce them as required if query is passed to the function

Example function
I would like query to be optional
but if query is passed, I want column and row to be required

// typescript

interface GoogleSheetsConfig {
    sheetId: string;
    query? string;
	column?: string;
	row?: string;
}

export const fetchGoogleSheet = async ({
	sheetId,
	query,
	column,
    row,
}: GoogleSheetsConfig) => {

// if query exists, include column and row
// else ignore query,
}

I found this answer that says it does what I'm trying to accomplish, but there is no explanation:
https://stackoverflow.com/questions/70522338/typescript-interface-property-want-to-change-optional-to-required-depending-on-o

答案1

得分: 3

以下是您要翻译的内容:

你想要 GoogleSheetsConfig 要么 包含所有 querycolumnrow要么 你希望它没有 query(而 columnrow 可以保持可选)。这种“要么-要么”的类型在 TypeScript 中最好通过 union 来表示。

为了实现这一点,让我们将原始的 GoogleSheetsConfig 类型重命名为 BaseGoogleSheetsConfig

interface BaseGoogleSheetsConfig {
  sheetId: string;
  query?: string;
  column?: string;
  row?: string;
}

现在,我们可以将上述提到的两种可能性表示为两种不同的类型。如果 query 存在,那么 columnrow 也存在。所以,让我们指定 GoogleSheetsConfigWithQuery 包含所有三个属性:

interface GoogleSheetsConfigWithQuery extends BaseGoogleSheetsConfig {
  query: string;
  column: string;
  row: string;
}

否则,query 不存在。我们实际上无法直接说属性必须不存在。相反,我们可以说它是 可选的(所以它 可能 不存在),它的值类型是 不可能的 never 类型(所以它本质上 不能 存在并具有定义的值)。根据您的编译器设置,它可能允许 undefined 存在。这是我们能够达到的最接近的方式;在实践中,这可能足够好。因此,让我们以这种方式定义 GoogleSheetsConfigWithoutQuery

interface GoogleSheetsConfigWithoutQuery extends BaseGoogleSheetsConfig {
  query?: never; 
}

因此,GoogleSheetsConfig 要么是 GoogleSheetsConfigWithQuery,要么是 GoogleSheetsConfigWithoutQuery

type GoogleSheetsConfig =
  GoogleSheetsConfigWithQuery | GoogleSheetsConfigWithoutQuery;

现在让我们看一下 fetchGoogleSheet 函数:

export const fetchGoogleSheet = async ({
  sheetId,
  query,
  column,
  row,
}: GoogleSheetsConfig) => {    
  column.toUpperCase(); // 错误,可能为 undefined
  if (typeof query !== "undefined") {
    column.toUpperCase(); // 已知为定义
    row.toUpperCase(); // 已知为定义
  }
}

在这里,您可以看到在测试 query 之前,column 的值可能为 undefined,而如果已知 query 已定义,那么 columnrow 也已知为定义。

这是因为 GoogleSheetsConfig 不仅仅是一个联合类型,而是一个 有区别的联合类型,其中 query区别 属性,因为 query 可能为 undefined,这是一个“单元类型”或“字面类型” ,并且 TypeScript 支持使用字段作为区别 只要它在联合的某个成员中具有字面类型

而且,幸运的是,TypeScript 支持对 解构的有区别的联合类型进行控制流分析。在 TypeScript 4.6 之前,您必须将函数参数保留为单个变量,例如 arg,然后检查 if (typeof arg.query !== "undefined") { arg.row.toUpperCase() }。但现在,您可以将其属性解构为单独的变量,仍然可以让编译器跟踪它们之间的关系。

英文:

You want GoogleSheetsConfig to either have all of query, column, and row present, or you want it to have query missing (and column and row can remain optional). That "either-or" sort of type is best represented in TypeScript by a union.

To write that, let's take your original GoogleSheetsConfig type and rename it out of the way to BaseGoogleSheetsConfig:

interface BaseGoogleSheetsConfig {
  sheetId: string;
  query?: string;
  column?: string;
  row?: string;
}

Now we can represent the two possibilities mentioned above as two separate types. If query is present, then so are column and row. So let's specify that a GoogleSheetsConfigWithQuery has all three present:

interface GoogleSheetsConfigWithQuery extends BaseGoogleSheetsConfig {
  query: string;
  column: string;
  row: string;
}

Otherwise, query is absent. We can't actually directly say that a property must be absent. Instead we can say that it is optional (so it may be absent) and that its value type is the impossible never type (so it essentially cannot be present with a defined value). Depending on your compiler settings it may allow undefined to be there. That's as close as we can get; in practice it is probably good enough. So let's define GoogleSheetsConfigWithoutQuery that way:

interface GoogleSheetsConfigWithoutQuery extends BaseGoogleSheetsConfig {
  query?: never; 
}

And so a GoogleSheetsConfig is either a GoogleSheetsConfigWithQuery or a GoogleSheetsConfigWithoutQuery:

type GoogleSheetsConfig =
  GoogleSheetsConfigWithQuery | GoogleSheetsConfigWithoutQuery;

Let's now look at the fetchGoogleSheet function:

export const fetchGoogleSheet = async ({
  sheetId,
  query,
  column,
  row,
}: GoogleSheetsConfig) => {    
  column.toUpperCase(); // error, might be undefined
  if (typeof query !== "undefined") {
    column.toUpperCase(); // known to be defined
    row.toUpperCase(); // known to be defined
  }
}

Here you can see that before we test query, the value of column might be undefined, while if query is known to be defined, then column and row are also known to be defined.

This works because GoogleSheetsConfig is not just a union, but a discriminated union, with query as the discriminant property, since query might be undefined, which is a "unit type" or a "literal type", and TypeScript supports using a field as a discriminant as long as it has a literal type in some member of the union.

And you're also lucky in that TypeScript supports control flow analysis for destructured discriminated unions. Before TypeScript 4.6 you'd have had to leave the function parameter as a single variable like arg and then check if (typeof arg.query !== "undefined") { arg.row.toUpperCase() }. But now you are able to destructure its properties into separate variables, and still have the compiler track the correlation between them.

Playground link to code

huangapple
  • 本文由 发表于 2023年6月15日 09:40:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/76478547.html
匿名

发表评论

匿名网友

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

确定