В начале был Javascript. Это язык высокого уровня, который поддерживает почти все. Это мультипарадигмальный, управляемый событиями, функциональный и императивный подход. ECMAScript эволюционировал, чтобы расширить использование Javascript. Сейчас это одна из основных технологий Интернета. По состоянию на август 2022 года 98% веб-сайтов используют Javascript в качестве клиентского языка программирования. Его эволюция распространяется на NodeJS и становится очень популярным благодаря невероятной производительности с асинхронным вводом-выводом.

Хронология 25 успешных лет Javascript

Сегодня веб-сайты превратились в нечто большее, чем просто информационные страницы. Важно, чтобы Javascript был более надежным. Нам нужно, чтобы язык стал устойчивым и простым для крупномасштабной разработки. Так родился TypeScript.

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

При переходе в мою следующую компанию у нас не было TypeScript. Я был счастлив иметь возможность кодировать, не беспокоясь об этих ошибках компиляции. В конце концов мы начали новый проект, в котором TypeScript снова был на переднем крае. Как и другие инженеры, мы все изучали TypeScript с нуля в очень больших масштабах (читали сотни миллионов пользователей ежедневно). Прибыв в DoorDash, TypeScript был здесь уже давно. Я смог увидеть зрелость языка и его использования. Со временем я увлекся языком. Сегодня я уже не могу представить проект без TypeScript.

В этой статье мы обсудим одну из самых замечательных функций TypeScript. Конечно, это только мое мнение, и я всегда в поиске других интересных качеств этого языка.

Базовая проверка типов

Объявив тип, мы можем обеспечить проверку типов для params при вызове нашей функции.

type TParams = {
  first: string | number
  second: number
}
type TFunc = (params: TParams) => void
const func: TFunc = ({ first, second }) => {
  console.log(first, second)
}
func({
  first: '1',
  second: 2
}) // Good
func({
  first: '1',
  second: '2'
}) // Bad

Первый вызов функции хорош тем, что оба свойства (first и second) имеют правильные типы. Второй вызов будет иметь ошибку компиляции из-за того, что свойство second передается как string.

Это самая простая проверка типов, и ее должен знать каждый, кто использует TypeScript.

Используйте дженерики для принудительного применения типов

Общие можно использовать для расширения типа, чтобы сделать его более пригодным для повторного использования. Ниже приведен пример объявления TParams и TFunc как двух типов с помощью универсальных шаблонов.

type TParams<T extends string | number> = {
  first: T
  second: T
}
type TFunc = <T extends string | number>(params: TParams<T>) => void
const func: TFunc = ({ first, second }) => {
  console.log(first, second)
}

Если мы вызовем функцию, как раньше

func({
  first: 1,
  second: 2
}) // Good
func({
  first: '1',
  second: '2'
}) // Good
func({
  first: 1,
  second: '2'
}) // Bad
func({
  first: '1',
  second: 2
}) // Bad

Типы здесь будут подразумеваться и следовать за типом переменной first. Это заставит first и second иметь один и тот же тип string или number.

Ниже приведена ошибка последнего вызова функции.

TParams также обеспечит, чтобы параметр объекта имел оба ключа.

Дополнительные ключи

Что, если мы хотим передать параметр объекта, для которого требуются определенные ключи, а другие являются необязательными?

Обновив наш тип TParams с помощью second в качестве необязательного аргумента, мы можем сделать так, чтобы ошибка исчезла.

type TParams<T extends string | number> = {
  first: T
  second?: T
}

Условные параметры

Но TypeScript не был бы суперполезным, если бы мы не могли легко расширять наши функции. Давайте введем нашу функцию enforceKey ниже

type TParams<T extends string | number> = {
  first: T
  second: T
}
const enforceKey = <T>(
  params: keyof T extends 'first' | 'second' ? TParams<string> : T
) => {
  console.log(params)
}

Это приведет к ошибкам всякий раз, когда мы используем enforceKey как

enforceKey({ first: '1' }) // Bad
enforceKey({ second: '1' }) // Bad
enforceKey({ first: '1', second: '2' }) // Good

Используя keyof, он будет обеспечивать, чтобы каждый раз, когда у нас есть объект в качестве параметра, он проверял, есть ли у нас ключ first и по умолчанию TParams в качестве его типа, иначе он по умолчанию будет использовать все, что передано в параметре. Тем не менее, это имеет некоторые проблемы

enforceKey({ first: '1', third: '3' }) // Good

Несмотря на то, что наш параметр здесь имеет first в качестве ключа, он не интерпретирует его как тип TParams<string>. Как мы можем применить это дальше, чтобы, пока он имеет first в качестве ключа, он был принудительно переведен в TParams<string>

Немедленной мыслью было бы попробовать использовать тип соединения.

type TParams<T extends string | number> = {
  first: T
  second: T
}
type TParams1 = {
  third: string
  forth: string
}
const enforceKey = (params: TParams<string> | TParams1): void => {
  console.log(params)
}

Это приведет к ошибке компиляции желания при вызове с параметром { first: ‘1’, third: ‘3’ }. Однако, когда мы пытаемся получить доступ к ключу first в нашем params, мы получаем ошибку компиляции.

Перегрузка функций

Это концепция, согласно которой мы можем объединить несколько функций в одну. В приведенном ниже примере будет создана функция с +1 overload.

type TParams<T extends string | number> = {
  first: T
  second: T
}
type TParams1 = {
  third: string
  forth: string
}
function enforceKey(params: TParams<string>) : void
function enforceKey(params: TParams1): void
function enforceKey<T>(params: T): void {
  console.log(params)
}

Теперь все наши проверки будут выполнены правильно :)

enforceKey({ first: '1', second: '2' }) // Good
enforceKey({ third: '3', forth: '4' }) // Good
enforceKey({ first: '1' }) // Bad
enforceKey({ second: '1' }) // Bad
enforceKey({ first: '1', third: '3' }) // Bad

Давайте конвертируем в старый добрый тип const

const enforceKey: {
  (params: TParams<string>): void
  (params: TParams1): void
} = <T>(params: T): void => {
  console.log(params.first)
  console.log(params.third)
}

И снова наша ошибка компиляции будет отображаться правильно :)

enforceKey({ first: '1', second: '2' }) // Good
enforceKey({ third: '3', forth: '4' }) // Good
enforceKey({ first: '1' }) // Bad
enforceKey({ second: '1' }) // Bad
enforceKey({ first: '1', third: '3' }) // Bad

Это не идеально. Если мы попытаемся получить доступ к свойству в params, у нас все еще будет та же ошибка, что и при предыдущей попытке с типом объединения.

Property 'first' does not exist on type 'T'.

Чтобы противодействовать этому, нам нужно обновить наш тип T в функции другим типом объединения.

const enforceKey: {
  (params: TParams<string>):  void
  (params: TParams1): void
} = <T extends TParams<string> & TParams1>(params: T): void => {
  console.log(params.first)
  console.log(params.third)
}

Теперь наши проверки на first и third будут проходить корректно и с честью.

Заключение

Сказать, что я люблю TypeScript, было бы преуменьшением! Хотя использование TypeScript было радостью, я знаю, что ему нужно учиться. Я видел аргументы от других о наличии большого количества шаблонов, которые могут замедлить разработку для новых пользователей. Тем не менее, я все же рекомендую изучить основы. Благодаря поддержке статической/строгой типизации проверка типов выявляет ошибки во время разработки. Между тестами TypeScript и unit/e2e мы можем создавать более качественные приложения.

И, конечно же, заходите в мой блог для будущих еженедельных сообщений, если вы найдете это полезным.