TypeScript 5 中带有基于条件类型的参数的泛型函数

问题描述 投票:0回答:1

我有一个带有一些不同配置对象的类型,如下所示:

type Types =
  | { kind: 'typeA', arg1: string }
  | { kind: 'typeB', arg1: string, arg2: string }

我还有一个类型,它只从上面的联合中取出

kind
子类型:

type InnerType = Types['kind'];

这里

innerType
正如预期的那样是
'typeA'|'typeb'
的并集。

最后,我有条件类型,它也从

Types
联合中仅提取非种类子类型:

type ExtractOtherParams<K, T> = K extends { kind: T }
  ? Omit<K, 'kind'>
  : never

到目前为止,这按预期工作 - 如果我创建一个名为

Test
的新类型并使用传入
typeA
的条件类型,则该类型是仅包含
arg

的对象
type Test = ExtractOtherParams<Types, 'typeA'> // test = { arg1: string }

如果我将

typeb
传递给条件,那么它会给出具有
arg1
arg2
属性的对象类型,例如:

type Test = ExtractOtherParams<Types, 'typeB'> // test = { arg1: string, arg2: string }

到目前为止,一切都很好。但是现在,当我尝试定义使用此条件的函数时,它无法按预期工作。例如:

function test<T extends InnerType>(
  kind: T,
  others: ExtractOtherParams<Types, T>,
): void {
switch (kind) {
  case 'typeA':
    console.log(others.arg1);
    break;
  case 'typeB':
    console.log(others.arg1, others.arg2);
}

这里大部分是正确的 - 我只能打开值

'typeA'
'typeB'
,任何其他不在原始类型联合中的值都会给出错误 - 这很好。但在第二种情况下,当我尝试访问
others.arg2
时,它给出错误(它说 arg2 在类型“ExtractOtherParams<{ kind: "typeA"; arg1: string; }, T>”上不存在)

但是,当我使用这个函数时,它会按照消费者 POV 的预期工作,例如,如果我使用

typeA
调用该函数,那么它只会让我使用 arg1:

传递对象
test('typeA', { arg1: 'test' }); // correct, no errors
test('typeA', { arg1: 'test', arg2: 'oops' }); // correct, has error on arg2
test('typeB', { arg1: 'test', arg2: 'works' }); // correct, no errors
test('typeB', { arg1: 'test' }); // correct, has error for missing arg2

我在

test
函数的定义中缺少什么以允许它访问第二个 case 表达式内的 arg2 ?

我尝试了不扩展的函数定义,仅使用泛型类型,例如:

test<T>
但这并没有什么区别。不确定还可以尝试什么

typescript conditional-types
1个回答
0
投票

问题是目前 TypeScript 不使用控制流分析来影响泛型类型参数。因此,如果您检查

kind
,您可能能够将
kind
的表观类型从
T
缩小到
T & "typeB"
。但
T
本身并没有改变。因此
ExtractOtherParams<Types, T>
不能等同于
ExtractOtherParams<Types, "typeB">
。所以你会得到这个错误。

关于此有各种未解决的问题;对于您的用例来说,似乎最规范的是 microsoft/TypeScript#33014。它已经开放有一段时间了,但有人希望它很快就能得到解决,因为 microsoft/TypeScript#56941 上有一个针对它的拉取请求。但除非它真正合并,否则我们必须解决您的问题。


一个简单的解决方法是通过一个 类型断言 来告诉 TypeScript 第二种情况下的

others
是预期的类型:

function test<T extends InnerType>(
    kind: T,
    others: ExtractOtherParams<Types, T>,
): void {
    switch (kind) {
        case 'typeA':
            console.log(others.arg1);
            break;
        case 'typeB':
            console.log(
                others.arg1,
                (others as ExtractOtherParams<Types, "typeB">).arg2
            );
    }
}

更复杂的是使用泛型进行重构,以便控制流分析很有帮助。您尝试在函数中使用的缩小类型将

kind
others
视为 discriminated union 的属性。您实际上可以编写
test()
,因此它使用 解构可区分联合 其余参数:

type TestArgs = Types extends infer T ? T extends Types ?
    [kind: T['kind'], others: Omit<T, "kind">] : never : never

/* type TestArgs = 
    [kind: "typeA", others: { arg1: string; }] | 
    [kind: "typeB", others: { arg1: string; arg2: string; }] 
*/

function test(...[kind, others]: TestArgs): void {
    switch (kind) {
        case 'typeA':
            console.log(others.arg1);
            break;
        case 'typeB':
            console.log(others.arg1, others.arg2); // okay
    }
}

test('typeA', { arg1: 'test' }); // correct, no errors
test('typeA', { arg1: 'test', arg2: 'oops' }); // correct, has error on arg2
test('typeB', { arg1: 'test', arg2: 'works' }); // correct, no errors
test('typeB', { arg1: 'test' }); // correct, has error for missing arg2

TestArgs
类型是通过Types上的
分布式条件类型
创建的,以获得
test()
的两个不同的预期调用签名。那么调用签名看起来像
(...[kind, others]: TestArgs) => void
,这意味着
[kind, others]
[kind: "typeA", others: { arg1: string; }]
类型,或者是
[kind: "typeB", others: { arg1: string; arg2: string; }]
类型。

现在,如果您检查

kind
,编译器就会理解
others
的含义。并且您的调用仍然会看到预期的类型检查。

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.