Допустим, у нас есть приложение, использующее кеш, и мы хотим написать небольшую вспомогательную функцию, которая может принимать запрос API на произвольную строку для ключа и проверять, существуют ли уже данные для этого ключа в кеше, если поэтому просто пропустите выполнение запроса и верните значение из кеша, в противном случае сделайте запрос, сохраните результат, а затем верните его. Мы могли бы написать что-то вроде этого:

const checkOrFetch = async(key, fetcher) => {
  try {
    // use key to check cache
    const cachedData = await checkCache(key);

    // if no cached data, run fetcher, cache result, and return
    if(cachedData) {
      return cachedData
    } else {
      const fetcherResult = await fetcher();
      addToCache(fetcherResult);
      return fetcherResult
    }
  } catch (error) {
    // do something with error
    return null
  }
}

В приведенном выше примере два наших аргумента неявно типизированы как any, поэтому везде, где используется этот помощник, мы оставляем себя открытыми для возможных ошибок, когда разработчик предполагает, что он собирается получить что-то от этой функции, на самом деле это может не получиться. . Тем не менее, с очень небольшой модификацией, мы можем не только сделать эту функцию безопасной для типов, но даже облегчить часть работы по вводу кода из кода, который будет реализовывать этот метод, все, что нам нужно сделать, это добавить некоторую общую типизацию в подпись.

const checkOrFetch = async <FetcherReturnType>(
  key: string,
  fetcher: () => Promise<FetcherReturnType>
): Promise<FetcherReturnType | null> => {
 ...
}

В примере имя FetcherReturnType совершенно произвольное, по сути это имя переменной. (В дикой природе вы часто будете видеть одиночные заглавные буквы, такие как T для типа, K для ключа, но давайте не будем бояться многословия, а вместо этого примем ясность.) Это небольшое изменение делает некоторые довольно интересные вещи. Теперь, пока сборщик, который передается в качестве второго аргумента, типизирован (даже неявно!), наша функция checkOrFetch сможет позволить разработчику, который ее использует, убедиться, что их код кошерный. Это немного отличается от того, как мы привыкли думать о функциях и их переменных, но синтаксис в примере означает что-то вроде:

В этой функции, которую я пишу, я знаю, что мне нужно знать о типе, это тип значения, которое она будет возвращать, я просто не могу знать, что это такое, потому что это будет зависеть от ему передается аргумент, так что давайте оставим для него место. Мы назовем его FetcherReturnType. В конце подписи мы делаем пометку, что каким бы ни было FetcherReturnType, мы вернем Promise, которое разрешается в него, но также может быть и null, если все пойдет не так.

Магия происходит со вторым аргументом. Я не знаю, точно что это будет, но я знаю, что мне нужна асинхронная функция, которая разрешает что-то. Поскольку Typescript довольно хорошо определяет тип возвращаемого значения из функции, все, что нужно сделать пользователю этого checkOrFetch метода, — это передать функцию, а Typescript позаботится обо всем остальном. Давайте посмотрим на некоторые примеры. Во-первых, горстка ненужных асинхронных функций, у которых действительно нет причин для существования, кроме как для этого урока:

const numberFetcher = async() => 1;
const stringFetcher = async() => 'string';
const boolFetcher = async() => true;
const objectFetcher = async() => ({
  foo: 'bar'
});

const getNumber = async() => checkOrFetch('numberKey', numberFetcher);
const getString = async() => checkOrFetch('stringKey', stringFetcher);
const getBool = async() => checkOrFetch('boolKey', boolFetcher);
const getObject = async() => checkOrFetch('objectKey', objectFetcher);

Если мы наведем курсор на геттеры в нашей IDE, мы увидим, что Typescript гарантирует, что мы знаем, что мы можем ожидать в ответ:

Это лишь малая часть того, что может сделать Typescript, но как только вы увидите, как он анализирует ваш код и выводит типы за вас, вы начнете задаваться вопросом, как вы раньше жили без него.