【发布时间】:2021-05-13 02:14:12
【问题描述】:
我正在努力减少该函数中函数参数的类型。在我看来,每当我执行if-check 将可能的值缩小为更小的子集时,类型检查器都会减少类型。令我惊讶的是,即使我明确检查泛型类型变量是否恰好是一个特定值,泛型类型也不会减少。
这是一个演示问题的示例(注意 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);
}
这只是来自https://github.com/unclechu/typescript-dependent-types-experiment/blob/master/index.ts 的副本。如您所见,这是一个依赖类型的示例。我希望返回类型 DbResponse<K> 取决于函数参数 DbRequest<K>。
让我们看看FIXMEs:
-
例子:
if (req.kind === 'DbRequestGetNewsList') { return [10,20,30] }失败:
Type 'number[]' is not assignable to type 'DbResponse<K>'.或者:
if (req.kind === 'DbRequestGetNewsItemById') { return 10 }失败:
Type 'number' is not assignable to type 'DbResponse<K>'.但我明确检查了种类,你可以看到条件:
K extends 'DbRequestGetNewsList' ? number[]以及K extends 'DbRequestGetNewsItemById' ? number。在示例中,您可以看到我将这些返回值转换为泛型类型 (
as DbResponse<K>),但这会杀死类型。例如我可以这样做:if (req.kind === 'DbRequestGetNewsList') { return 10 as DbResponse<K> } else if (req.kind === 'DbRequestGetNewsItemById') { return [10,20,30] as DbResponse<K> }这是完全错误的,类型检查器只是无声无息地吞下它。
-
你可以看到的下一个是
Property 'newsId' does not exist on type 'DbRequest<K>'.。实际上,这可以通过对
DbRequest<K>使用 sum-type 而不是类型条件来解决。但这会产生另一个问题,即调用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!
我相信这两个问题与同一来源有关,即dbQuery 函数体内的K 即使在显式if-条件检查单个特定K 之后也无法推断出这一事实.这真的是违反直觉的。它是否适用于任何情况但不适用于泛型?我能以某种方式克服这个问题并让类型检查器完成它的工作吗?
UPD #1
甚至不可能编写类型证明器:
function proveDbRequestGetNewsListKind<K extends DbRequestKind>(
req: DbRequest<K>
): req is DbRequest<'DbRequestGetNewsList'> {
return req.kind === 'DbRequestGetNewsList'
}
它失败了:
A type predicate's type must be assignable to its parameter's type.
Type '{ kind: "DbRequestGetNewsList"; }' is not assignable to type 'DbRequest<K>'.
更新 #2
最初我的解决方案是建立在重载之上的。它不能解决问题。见https://stackoverflow.com/a/66119805/774228
考虑一下:
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!')
}
}
此代码已损坏。不过类型检查器没问题。
重载的问题在于你不能为每个重载提供单独的实现。相反,您提供包含更大类型子集的通用实现。因此,您失去了类型安全性,它更容易出现运行时错误。
除此之外,您必须为每种类型手动提供越来越多的重载(就像在 Go 中一样,嗯)。
UPD #3
我通过添加一个带有类型转换的闭包来稍微改进了类型检查。它远非完美,但更好。
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
我使用@jcalz 下面提出的T[K] hack 稍微改进了最新示例(请参阅https://stackoverflow.com/a/66127276)。无需为每个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>
}
更新 #5
还有一项改进。我为闭包的返回类型添加了额外的约束。我还减少了模式中额外实体的数量。
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]
}
【问题讨论】:
-
TypeScript 没有依赖类型;我认为我能做的最多就是将您指向相关的 GitHub 问题,例如 microsoft/TypeScript#13995 并建议类型断言(您所谓的“强制转换”)可能是最接近您想要的行为的,因为编译器无法为您验证类型安全性。我可以看的更详细,但当人们似乎在这里投反对票时,我有点不愿意卷入这场争论中
-
@jcalz 谢谢你的回答,很高兴至少得到一个答案,这在技术上是不可能的。虽然我不明白对否决票的担忧。如果你不喜欢他们的工作方式,那就怪 SO。我的意思是赞成表示“有用”,反对表示“无用”,对吗?这就是人们如何知道哪个答案可以解决问题,哪个不能解决问题的方式,对吗?
标签: typescript generics types dependent-type