英文:
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
要么 包含所有 query
、column
和 row
,要么 你希望它没有 query
(而 column
和 row
可以保持可选)。这种“要么-要么”的类型在 TypeScript 中最好通过 union 来表示。
为了实现这一点,让我们将原始的 GoogleSheetsConfig
类型重命名为 BaseGoogleSheetsConfig
:
interface BaseGoogleSheetsConfig {
sheetId: string;
query?: string;
column?: string;
row?: string;
}
现在,我们可以将上述提到的两种可能性表示为两种不同的类型。如果 query
存在,那么 column
和 row
也存在。所以,让我们指定 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
已定义,那么 column
和 row
也已知为定义。
这是因为 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论