简体   繁体   English

TypeScript:如何使泛型类型在 function 中进行推断?

[英]TypeScript: How to make generic type to infer inside a function?

I'm struggling to reduce the type of a function argument inside that function.我正在努力减少 function 中的 function 参数的类型。 In my mind whenever I do an if -check, that shrinks possible values into smaller subset, the type checker reduces the type.在我看来,每当我进行if -check 将可能的值缩小为更小的子集时,类型检查器都会减少类型。 What was a surprise to me is that a generic type doesn't get reduced even when I explicitly check that the generic type variable is exactly one specific value.令我惊讶的是,即使我明确检查泛型类型变量是否恰好是一个特定值,泛型类型也不会减少。

Here is an example that demonstrates the problem (note the FIXME s):这是一个演示问题的示例(注意FIXME ):

type NewsId = number

type DbRequestKind =
    | 'DbRequestGetNewsList'
    | 'DbRequestGetNewsItemById'

type DbRequest<K extends DbRequestKind>
    = K extends 'DbRequestGetNewsList'     ? { kind: K }
    : K extends 'DbRequestGetNewsItemById' ? { kind: K, newsId: NewsId }
    : never;

type DbResponse<K extends DbRequestKind>
    = K extends 'DbRequestGetNewsList'     ? number[]
    : K extends 'DbRequestGetNewsItemById' ? number
    : never

function dbQuery<K extends DbRequestKind>(req: DbRequest<K>): DbResponse<K> {
    if (req.kind === 'DbRequestGetNewsList') {
        const result = [10,20,30]
        return result as DbResponse<K> // FIXME doesn’t check valid K
    } else if (req.kind === 'DbRequestGetNewsItemById') {
        // FIXME “Property 'newsId' does not exist on type 'DbRequest<K>'.”
        // const result = req.newsId + 10
        const result = 10
        return result as DbResponse<K> // FIXME doesn’t check valid K
    } else {
        throw new Error('Unexpected kind!')
    }
}

{
    const x = dbQuery({ kind: 'DbRequestGetNewsList' })

    // Check that response type is inferred
    const y: typeof x = [10]
    // const z: typeof x = 10 // fails (as intended, it’s good)

    console.log('DB response (list):', x);
}

{
    const x = dbQuery({ kind: 'DbRequestGetNewsItemById', newsId: 5 })

    // Check that response type is inferred
    // const y: typeof x = [10] // fails (as intended, it’s good)
    const z: typeof x = 10

    console.log('DB response (item by id):', x);
}

It's just a copy taken from https://github.com/unclechu/typescript-dependent-types-experiment/blob/master/index.ts .它只是取自https://github.com/unclechu/typescript-dependent-types-experiment/blob/master/index.ts的副本。 So as you can see this is an example of dependent typing.如您所见,这是一个依赖类型的示例。 I want return type DbResponse<K> to depend on the function argument DbRequest<K> .我希望返回类型DbResponse<K>取决于 function 参数DbRequest<K>

Let's look at FIXME s:让我们看一下FIXME

  1. Example:例子:

     if (req.kind === 'DbRequestGetNewsList') { return [10,20,30] }

    Fails with: Type 'number[]' is not assignable to type 'DbResponse<K>'.失败: Type 'number[]' is not assignable to type 'DbResponse<K>'.

    Or:或者:

     if (req.kind === 'DbRequestGetNewsItemById') { return 10 }

    Fails with: Type 'number' is not assignable to type 'DbResponse<K>'.失败: Type 'number' is not assignable to type 'DbResponse<K>'.

    But I explicitly check for the kind , and you can see the condition: K extends 'DbRequestGetNewsList'? number[]但是我明确检查了kind ,你可以看到条件: K extends 'DbRequestGetNewsList'? number[] K extends 'DbRequestGetNewsList'? number[] as well as K extends 'DbRequestGetNewsItemById'? number K extends 'DbRequestGetNewsList'? number[]以及K extends 'DbRequestGetNewsItemById'? number K extends 'DbRequestGetNewsItemById'? number . K extends 'DbRequestGetNewsItemById'? number

    In the example you can see that I'm casting those returned values to generic type ( as DbResponse<K> ) but this kills the types.在示例中,您可以看到我将这些返回值转换为泛型类型( as DbResponse<K> ),但这会杀死类型。 For instance I can do this:例如我可以这样做:

     if (req.kind === 'DbRequestGetNewsList') { return 10 as DbResponse<K> } else if (req.kind === 'DbRequestGetNewsItemById') { return [10,20,30] as DbResponse<K> }

    Which is totally wrong and the type checker just swallows it with no sound.这是完全错误的,类型检查器只是吞下它而没有声音。

  2. Next one that you can see is Property 'newsId' does not exist on type 'DbRequest<K>'.您可以看到的下一个是Property 'newsId' does not exist on type 'DbRequest<K>'. . .

    Actually this can be fixed by using sum-type for DbRequest<K> instead of type conditions.实际上,这可以通过对DbRequest<K>使用 sum-type 而不是类型条件来解决。 But this would create another problem where a call of dbQuery would return generic type again instead of inferring it, thus:但这会产生另一个问题,即调用dbQuery将再次返回泛型类型而不是推断它,因此:

     const x = dbQuery({ kind: 'DbRequestGetNewsList' }) const y: typeof x = [10] const z: typeof x = 10 // FIXME This must fail but it doesn't with sum-type!

I believe these two problems are connected to the same source, to the fact that K inside the body of dbQuery function can't be inferred even after explicit if -condition check for single particular K .我相信这两个问题与同一来源有关,即即使在对单个特定K进行显式if检查后,也无法推断出dbQuery function 主体内部的K This is really counterintuitive.这真的是违反直觉的。 Does it work for any case but not for generics?它适用于任何情况,但不适用于 generics? Can I overcome this somehow and make the type checker to do its job?我能以某种方式克服这个问题并让类型检查器完成它的工作吗?

UPD #1 UPD #1

It's even impossible to write a type prover:甚至不可能编写类型证明器:

function proveDbRequestGetNewsListKind<K extends DbRequestKind>(
    req: DbRequest<K>
): req is DbRequest<'DbRequestGetNewsList'> {
    return req.kind === 'DbRequestGetNewsList'
}

It fails with:它失败了:

A type predicate's type must be assignable to its parameter's type.
  Type '{ kind: "DbRequestGetNewsList"; }' is not assignable to type 'DbRequest<K>'.

UPD #2 UPD #2

Initially my solution was built on top of overloads.最初我的解决方案是建立在重载之上的。 It doesn't solve the problem.它不能解决问题。 See https://stackoverflow.com/a/66119805/774228https://stackoverflow.com/a/66119805/774228

Consider this:考虑一下:

function dbQuery(req: DbRequest): number[] | number {
    if (req.kind === 'DbRequestGetNewsList') {
        return 10
    } else if (req.kind === 'DbRequestGetNewsItemById') {
        return [10,20,30]
    } else {
        throw new Error('Unexpected kind!')
    }
}

This code is broken.此代码已损坏。 Type checker is okay with it though.类型检查器虽然可以。

The problem with overloads is that you can't provide separate implementation for each overload.重载的问题是您不能为每个重载提供单独的实现。 Instead you provide generic implementation which includes bigger subset of types.相反,您提供包含更大类型子集的通用实现。 Thus you loose type safety, it gets more runtime error-prone.因此,您失去了类型安全性,它更容易出现运行时错误。

Apart from this you have to manually provide more and more overloads, for each type (just like in Go, meh).除此之外,您必须为每种类型手动提供越来越多的重载(就像在 Go 中一样)。

UPD #3 UPD #3

I've improved type checking a bit by adding a closure with type casting.我通过添加一个带有类型转换的闭包来稍微改进了类型检查。 It's far from perfect but better.它远非完美,但更好。

function dbNewsList(
    req: DbRequest<'DbRequestGetNewsList'>
): DbResponse<'DbRequestGetNewsList'> {
    return [10, 20, 30]
}

function dbNewsItem(
    req: DbRequest<'DbRequestGetNewsItemById'>
): DbResponse<'DbRequestGetNewsItemById'> {
    return req.newsId + 10
}

function dbQuery<K extends DbRequestKind>(req: DbRequest<K>): DbResponse<K> {
    return (req => {
        if (req.kind === 'DbRequestGetNewsList') {
            return dbNewsList(req)
        } else if (req.kind === 'DbRequestGetNewsItemById') {
            return dbNewsItem(req)
        } else {
            throw new Error('Unexpected kind!')
        }
    })(
        req as DbRequest<'DbRequestGetNewsList' | 'DbRequestGetNewsItemById'>
    ) as DbResponse<K>;
}

UPD #4 UPD #4

I slightly improved the latest example using T[K] hack which was proposed by @jcalz below (see https://stackoverflow.com/a/66127276 ).我使用下面@jcalz 提出的T[K] hack 稍微改进了最新示例(请参阅https://stackoverflow.com/a/66127276 )。 No need for additional functions for each kind .无需为每种kind添加额外的功能。

type NewsId = number

type DbRequestKind = keyof DbResponseMap

type DbRequest<K extends DbRequestKind>
    = K extends 'DbRequestGetNewsList'     ? { kind: K }
    : K extends 'DbRequestGetNewsItemById' ? { kind: K, newsId: NewsId }
    : never

interface DbResponseMap {
    DbRequestGetNewsList: number[]
    DbRequestGetNewsItemById: number
}

type DbResponse<K extends DbRequestKind> = DbResponseMap[K]

function dbQuery<K extends DbRequestKind>(req: DbRequest<K>): DbResponse<K> {
    return (req => {
        if (req.kind === 'DbRequestGetNewsList') {
            const result: DbResponseMap[typeof req.kind] = [10, 20, 30]
            return result
        } else if (req.kind === 'DbRequestGetNewsItemById') {
            const result: DbResponseMap[typeof req.kind] = req.newsId + 10
            return result
        } else {
            const _: never = req
            throw new Error('Unexpected kind!')
        }
    })(req as DbRequest<DbRequestKind>) as DbResponse<K>
}

UPD #5 UPD #5

One more improvement.还有一项改进。 I added additional constraint for returned type of the closure.我为闭包的返回类型添加了额外的约束。 Also I reduced amount of extra entities in the pattern.我还减少了模式中额外实体的数量。

type NewsId = number

type DbRequest<K extends keyof DbResponseMap>
    = K extends 'DbRequestGetNewsList'     ? { kind: K }
    : K extends 'DbRequestGetNewsItemById' ? { kind: K, newsId: NewsId }
    : never

interface DbResponseMap {
    DbRequestGetNewsList: number[]
    DbRequestGetNewsItemById: number
}

function dbQuery<K extends keyof DbResponseMap>(req: DbRequest<K>): DbResponseMap[K] {
    return ((req): DbResponseMap[keyof DbResponseMap] => {
        if (req.kind === 'DbRequestGetNewsList') {
            const result: DbResponseMap[typeof req.kind] = [10, 20, 30]
            return result
        } else if (req.kind === 'DbRequestGetNewsItemById') {
            const result: DbResponseMap[typeof req.kind] = req.newsId + 10
            return result
        } else {
            const _: never = req
            throw new Error('Unexpected kind!')
        }
    })(req as DbRequest<keyof DbResponseMap>) as DbResponseMap[K]
}

As mentioned in the comments, TypeScript doesn't really have support for dependent types, especially not when it comes to type checking the implementation of a function whose call signature implies such a dependency.正如评论中提到的,TypeScript 并不真正支持依赖类型,尤其是在检查 function 的实现时,它的调用签名暗示了这种依赖关系。 The general problem you're facing is mentioned in a number of GitHub issues, notably microsoft/TypeScript#33014 and microsoft/TypeScript#27808 .在许多 GitHub 问题中都提到了您面临的一般问题,特别是microsoft/TypeScript#33014microsoft/ TypeScript#27808 。 Currently the two main ways to go here are: write an overloaded function and be careful with the implementation, or to use a generic function with type assertions and be careful with the implementation.目前 go 的两种主要方法是:编写重载的 function 并小心实现,或使用通用 function 并小心输入断言。


Overloads:重载:

For overloads, the implementation is intentionally checked more loosely than the set of call signatures.对于重载,故意比调用签名集更宽松地检查实现。 Essentially as long as you return a value that at least one of the call signatures expects, there will not be an error for that return value.本质上,只要您返回至少一个调用签名所期望的值,该返回值就不会出现错误。 This is, as you saw, unsafe.如您所见,这是不安全的。 It turns out that TypeScript is not fully type safe or sound;事实证明,TypeScript 不是完全安全或健全的; in fact this is explicitly not a TypeScript language design goal.事实上,这显然不是TypeScript 语言设计目标。 See non-goal #3:非目标#3:

  1. Apply a sound or "provably correct" type system.应用健全或“可证明正确”的类型系统。 Instead, strike a balance between correctness and productivity.相反,在正确性和生产力之间取得平衡。

Inside the implementation of an overloaded function, the TS team favors productivity over correctness.在重载 function 的实现中,TS 团队更注重生产力而不是正确性。 It is essentially the implementer's job to guarantee type safety;保证类型安全本质上是实现者的工作; the compiler does not really attempt to do it.编译器并没有真正尝试这样做。

See microsoft/TypeScript#13235 for more information.有关详细信息,请参阅microsoft/TypeScript#13235 It was suggested to catch such errors, but the suggestion was closed as "Too Complex".有人建议捕获此类错误,但该建议被关闭为“太复杂”。 Doing overloads "the right way" would require much more work for the compiler and there isn't enough evidence that these sorts of mistakes are made often enough to make the added complexity and performance penalty worthwhile.以“正确的方式”进行重载需要编译器做更多的工作,并且没有足够的证据表明这类错误经常发生,足以使增加的复杂性和性能损失值得。


Generic functions:通用函数:

The problem here is sort of the opposite;这里的问题恰恰相反。 the compiler is unable to see that the implementation is safe, and will give you an error for just about anything you return.编译器无法看到实现是安全的,并且会为您返回的任何内容提供错误。 Control flow analysis does not narrow an unresolved generic type parameter or a value of an unresolved generic type.控制流分析不会缩小未解析的泛型类型参数或未解析的泛型类型的值。 You can check req.kind , but this is not used by the compiler to do anything to the type of K .您可以检查req.kind ,但编译器不会使用它来对K的类型做任何事情。 Arguably you cannot narrow K by checking a value of type K because it might still be the full union.可以说,您不能通过检查K类型的值来缩小K ,因为它可能仍然是完整的联合。

See microsoft/TypeScript#24085 for more discussion about this issue.有关此问题的更多讨论,请参阅microsoft/TypeScript#24085 Doing this "the right way" would require some fundamental changes to the way generics are handled.这样做“正确的方式”需要对 generics 的处理方式进行一些根本性的改变。 It's an open issue at least, so there's some hope that something might be done in the future, but I wouldn't rely on it.至少这是一个悬而未决的问题,所以有一些希望将来可能会做一些事情,但我不会依赖它。

If you want the compiler to accept something it cannot verify, you should double-check that you're doing it right and then use a type assertion to silence the compiler warning.如果你想让编译器接受它无法验证的东西,你应该仔细检查你做对了,然后使用类型断言来消除编译器警告。


For your specific example we can do a little better.对于您具体示例,我们可以做得更好。 One of the few places TypeScript does an attempt to model dependent types is when looking up an object property type from a literal key type. TypeScript 尝试 model 依赖类型的少数地方之一是从文字键类型查找object 属性类型时。 If you have a value t of type T , and a key k of type K extends keyof T , then the compiler will understand that t[k] is of type T[K] .如果您有一个类型为T的值t ,并且类型为 K 的键k K extends keyof T ,那么编译器将理解t[k]的类型为T[K]

Here's how we can rewrite what you're doing to take the form of such an object property lookup:以下是我们如何重写您正在执行的操作以采用此类 object 属性查找的形式:

interface DbRequestMap {
  DbRequestGetNewsList: {};
  DbRequestGetNewsItemById: { newsId: NewsId }
}
type DbRequestKind = keyof DbRequestMap;
type DbRequest<K extends DbRequestKind> = DbRequestMap[K] & { kind: K };

interface DbResponseMap {
  DbRequestGetNewsList: number[];
  DbRequestGetNewsItemById: number;
}
type DbResponse<K extends DbRequestKind> = DbResponseMap[K]

function dbQuery<K extends DbRequestKind>(req: DbRequest<K>): DbResponse<K> {
  return {
    get DbRequestGetNewsList() {
      return [10, 20, 30];
    },
    get DbRequestGetNewsItemById() {
      return 10; 
    }
  }[req.kind];
}

Here we represent DbRequest<K> as a value with a {kind: K} property and DbResponse<K> as a value of type DbResponseMap[K] .在这里,我们将DbRequest<K>表示为具有{kind: K}属性的值,将DbResponse<K>表示为DbResponseMap[K]类型的值。 In the implementation we make an object of type DbResponseMap with getters to prevent the entire object from being calculated, and then look up its req.kind property of type K ... to get a DbResponse<K> the compiler is happy with.在实现中,我们使用getter来创建一个 DbResponseMap 类型的DbResponseMap以防止计算整个 object,然后查找其类型为K ... 的req.kind属性以获得编译器满意的DbResponse<K>

It's not perfect by a long shot though.不过,从长远来看,它并不完美。 Inside the implementation the compiler still cannot narrow req to anything that has, say, a newsId property.在实现内部,编译器仍然无法将req缩小到具有newsId属性的任何内容。 So you'll find yourself still narrowing unsafely:所以你会发现自己仍然不安全地缩小范围:

return (req as DbRequest<DbRequestKind> as 
  DbRequest<"DbRequestGetNewsItemById">).newsId + 10; // 🤮

So I think in practice you should just pick your poison and deal with type safety being violated somewhere inside your implementation.所以我认为在实践中你应该选择你的毒药并处理在你的实现中某处违反类型安全的问题。 If you're careful you can at least maintain type safety for the callers of your function, which is the best we can hope for with TypeScript 4.1 at any rate.如果您小心,您至少可以为您的 function 的调用者保持类型安全,无论如何,这是我们希望 TypeScript 4.1 所能达到的最佳效果。


Playground link to code Playground 代码链接

Here you have working code:在这里你有工作代码:

type NewsId = number

type DbRequestKind =
  | 'DbRequestGetNewsList'
  | 'DbRequestGetNewsItemById'

type DbRequest<K extends DbRequestKind>
  = K extends 'DbRequestGetNewsList' ? { kind: K }
  : K extends 'DbRequestGetNewsItemById' ? { kind: K, newsId: NewsId }
  : never;

type DbResponse<K extends DbRequestKind>
  = K extends 'DbRequestGetNewsList' ? number[]
  : K extends 'DbRequestGetNewsItemById' ? number
  : never

type Distributive<T> = [T] extends [any] ? T : never


function dbQuery<K extends DbRequestKind>(req: DbRequest<'DbRequestGetNewsItemById'>): DbResponse<'DbRequestGetNewsItemById'>
function dbQuery<K extends DbRequestKind>(req: DbRequest<'DbRequestGetNewsList'>): DbResponse<'DbRequestGetNewsList'>
function dbQuery(req: DbRequest<DbRequestKind>): Distributive<DbResponse<DbRequestKind>> {
  if (req.kind === 'DbRequestGetNewsList') {
    const result = [10, 20, 30]
    return result // FIXME doesn’t check valid K
  } else if (req.kind === 'DbRequestGetNewsItemById') {
    const result = req.newsId + 10 // error
    //return '2' // error
    return 2 // error
  } else {
    const x = req // never
    throw new Error('Unexpected kind!')
  }
}

Please keep in mind, that K extends DbRequestKind is not the same as DbRequestKind , because K can be much wider.请记住, K extends DbRequestKindDbRequestKind ,因为 K 可以更宽。 This made the trick这成功了

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

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