#typescript #typescript-generics
Вопрос:
С этим кодом, я думаю, ясно, чего я хочу достичь, есть какие-нибудь идеи, чтобы попытаться получить код, как ожидалось?
interface Person {
name: "Peter" | "Robert";
age: number;
isPerson: true
}
interface Animal {
name: "Jerry" | "Tom";
age: number;
isPerson: false
}
type LivingBeing = Person | Animal
function getRandomName<T extends LivingBeing>(isPerson: T["isPerson"]): T["name"] {
const names: T["name"][] = isPerson ? ["Peter", "Robert"] : ["Jerry", "Tom"]
return names[Math.floor(Math.random()*names.length)];
}
const person: "Peter" | "Robert" = getRandomName(true) // Only valid values are "Peter" | "Robert". Not "Peter" | "Robert" | "Jerry" | "Tom" as the compiler says. Check it in the Playground TS
Ответ №1:
ПРОБЛЕМА: НЕТ САЙТА ВЫВОДА ДЛЯ T
Проблема, с которой вы столкнулись, заключается в том, что в следующей подписи вызова,
declare function getRandomName<T extends LivingBeing>(isPerson: T["isPerson"]): T["name"];
для параметра универсального типа не существует хорошего сайта вывода T
. Вы надеетесь , что компилятор посмотрит на isPerson
тип T["isPerson"]
и использует его для вывода T
. К сожалению, это не так; индексированные типы доступа формы T[K]
не могут использоваться для вывода или T
или K
.
В какой-то момент был запрос на вывод от одного из членов команды TS, microsoft/TypeScript#20126, который добавил бы поддержку для этого. Увы, он никогда не был объединен в основную ветвь и не является частью языка. Из истории вопроса неясно, почему. Ну что ж.
Без сайта вывода для параметра универсального типа T
, и если вы не укажете его вручную, например
getRandomName<Person>(true);
тогда вывод завершится ошибкой, и компилятор вернется к своему ограничению:
getRandomName(true);
// function getRandomName<LivingBeing>(
// isPerson: boolean): "Peter" | "Robert" | "Jerry" | "Tom"
Поскольку T
был вынужден LivingBeing
, это выводится как LivingBeing
, и LivingBeing["isPerson"]
справедливо boolean
, и LivingBeing["name"]
является тем объединением четырех имен. И вы потеряли всякую связь между true
и false
и типом вывода. Ой.
РЕШЕНИЕ: ПРЕДОСТАВЬТЕ САЙТ ВЫВОДА ДЛЯ T
Если вы хотите, чтобы вызовы getRandomName()
правильно выводили параметр типа, вам нужно будет предоставить компилятору удобный сайт для вывода для него. Самый простой и простой способ сделать это-сделать параметр типа точно таким же, как тип переданного параметра функции. Вы проходите isPerson
, поэтому давайте сделаем тип isPerson
параметром типа, который мы будем вызывать P
. Нам придется ограничиться P
допустимыми типами для isPerson
, которые LivingBeing["isPerson"]
(также называются boolean
, но мы оставим это так). Таким образом, подпись вызова будет выглядеть примерно так:
declare function getRandomName<P extends LivingBeing["isPerson"]>(
isPerson: P
): ???;
Вместо ???
этого нам нужно рассчитать желаемый тип вывода. Во-первых, учитывая P
, какой член Person
союза подходит? По сути, это дискриминация дискриминируемого объединения на уровне типов. Для этого мы можем использовать условный тип: в частности, Extract<T, U>
тип утилиты:
// this is just to demonstrate; we don't need to use the type alias
type DiscriminateLivingBeing<P extends LivingBeing["isPerson"]> =
Extract<LivingBeing, { isPerson: P }>;
type TrueType = DiscriminateLivingBeing<true> // Person
type FalseType = DiscriminateLivingBeing<false> // Animal
И как только мы это получим, нам просто нужно будет посмотреть name
недвижимость. Итак, вот новая подпись вызова:
declare function getRandomName<P extends LivingBeing["isPerson"]>(
isPerson: P
): Extract<LivingBeing, { isPerson: P }>["name"];
И мы можем это проверить:
const person = getRandomName(true)
// const person: "Peter" | "Robert"
Выглядит неплохо. Мы закончили, верно? Э-э, не совсем:
НЕДОСТАТОК: ОБЩИЕ УСЛОВНЫЕ ПОДПИСИ ВЫЗОВОВ ТРУДНО РЕАЛИЗОВАТЬ
Однако, если вы попытаетесь реализовать getRandomName()
все точно так же, как и раньше, вы столкнетесь с проблемой:
function getRandomName<P extends LivingBeing["isPerson"]>(
isPerson: P
): Extract<LivingBeing, { isPerson: P }>["name"] {
const names: Extract<LivingBeing, { isPerson: P }>["name"][] = // error!
isPerson ? ["Peter", "Robert"] : ["Jerry", "Tom"]
return names[Math.floor(Math.random() * names.length)]; // error!
}
Внутри реализации функции P
находится неразрешенный или неопределенный параметр универсального типа. И тип Extract<LivingBeing, {isPerson: P}>["name"][]
-это условный тип, который зависит от него. И когда условный тип зависит от параметра неразрешенного типа, компилятор, по сути, отказывается от попыток понять, может ли ему быть присвоено какое-либо конкретное значение. Это полностью откладывает оценку. Таким образом , в то время как человек мог бы пройти через различные возможности P
и убедить себя в том, что isPerson ? ["Peter", "Robert"] : ["Jerry", "Tom"]
это возможно Extract<LivingBeing, {isPerson: P}>["name"][]
, компилятор не в состоянии этого сделать. Поэтому он жалуется.
В microsoft/TypeScript#33912 есть запрос на функцию, в котором запрашивается лучшая поддержка для реализации функций, подписи вызовов которых включают общие условные типы. Однако на данный момент вам нужно обойти это с помощью чего-то вроде утверждения типа, чтобы отключить предупреждение компилятора.
Обычно в подобных случаях я делаю функцию перегрузкой с помощью одной подписи вызова. Реализации операторов функции перегрузки проверяются более свободно, поэтому я делаю подпись вызова полностью правильной:
// call signature
function getRandomName<P extends LivingBeing["isPerson"]>(
isPerson: P
): Extract<LivingBeing, { isPerson: P }>["name"];
И затем типы ввода и вывода реализации функции расширяются достаточно, чтобы заставить реализацию работать:
// implementation
function getRandomName(isPerson: LivingBeing["isPerson"]): LivingBeing["name"] {
const names = isPerson ? ["Peter", "Robert"] as const : ["Jerry", "Tom"] as const
return names[Math.floor(Math.random() * names.length)];
}
Я использовал const
утверждения, чтобы компилятор отслеживал типы строковых литералов строк в массиве. Но в остальном это простая реализация. Если вы действительно хотите, вы могли бы ослабить его еще больше и обойтись даже без этого:
// implementation
function getRandomName(isPerson: boolean): string {
const names = isPerson ? ["Peter", "Robert"] : ["Jerry", "Tom"]
return names[Math.floor(Math.random() * names.length)];
}
В любом случае, теперь ошибки нет, и вы можете вызывать функцию так, как хотите:
const person = getRandomName(true)
// const person: "Peter" | "Robert"
const animal = getRandomName(false)
// const animal: "Jerry" | "Tom"
Комментарии:
1. Ух ты! Спасибо за такой обстоятельный ответ. Я понял большую часть ответа, но мне нужно некоторое время, чтобы действительно разобраться в нем более подробно. А также большое вам спасибо за предоставление кода