Я изо всех сил пытаюсь уменьшить тип аргумента функции внутри этой функции. На мой взгляд, всякий раз, когда я выполняю if
-проверку, которая сокращает возможные значения до меньшего подмножества, средство проверки типов уменьшает тип. Что меня удивило, так это то, что общий тип не уменьшается даже тогда, когда я явно проверяю, что переменная универсального типа представляет собой ровно одно конкретное значение.
Вот пример, демонстрирующий проблему (обратите внимание на 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>
.
Давайте посмотрим на FIXME s:
Пример:
if (req.kind === 'DbRequestGetNewsList') { return [10,20,30] }
Не удается:
Type 'number[]' is not assignable to type 'DbResponse<K>'.
Or:
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>
вместо условий типа. Но это создало бы еще одну проблему, когда вызов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!
Я считаю, что эти две проблемы связаны с одним и тем же источником, с тем фактом, что K
внутри тела dbQuery
функции не может быть выведено даже после явной проверки 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>'.
UPD # 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
Я немного улучшил последний пример, используя хак T[K]
, который был предложен @jcalz ниже (см. 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>
}
UPD # 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]
}