Работа с универсальными типами и типами объединений

#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. Ух ты! Спасибо за такой обстоятельный ответ. Я понял большую часть ответа, но мне нужно некоторое время, чтобы действительно разобраться в нем более подробно. А также большое вам спасибо за предоставление кода