Фридайвинг — ныряние на глубину без акваланга. Ныряльщик ощущает на себе закон Архимеда: он вытесняет некоторый объём воды, который выталкивает его обратно. Поэтому первые несколько метров даются тяжелее всего, но потом сила давления толщи воды над тобой начинает помогать двигаться глубже. Этот процесс напоминает изучение и погружение в системы типов TypeScript — по мере погружения становится немного легче. Но надо не забыть вовремя вынырнуть.
Фотография с сайта One Ocean One Breath.
Михаил Башуров (saitonakamura) — Senior Frontend Engineer в компании WiseBits, фанат TypeScript и фридайвер-любитель. Аналогии изучения TypeScript и ныряния на глубину не случайны. Михаил расскажет, что такое discriminated unions, как использовать вывод типов, зачем нужна номинальная совместимость и брендирование. Задержите дыхание и погружайтесь.
Презентация и ссылки на GitHub с кодом примеров здесь.
Типы суммы звучит алгебраично — попробуем разобраться, что это такое. В ReasonML они называются variant, в Haskell — tagged union, а в F# и в TypeScript — discriminated union.
Википедия даёт такое определение: «Тип суммы — это сумма типов произведений».
— Спасибо капитан!
Определение донельзя корректно, но бесполезно, поэтому давайте разбираться. Пойдем от частного и начнём с типа произведения.
Предположим, что у нас есть тип
Это тип произведения — пересечение или intersection. Это значит, что мы можем записать тот же код, как каждое поле по отдельности.
В алгебре высказываний это называется логическим умножением: это операция «И» или «AND». Тип произведения — логическое произведение этих двух элементов.
Добавим его в
Объединение — union — пригодится, например, для представления типа переменной, которая задаёт ширину элемента в CSS: строки в 10 пикселей или абсолютного значения. На самом деле спецификация CSS гораздо сложнее, но для простоты давайте оставим так.
Пример сложнее. Предположим, у нас есть задача. Она может находиться в трех состояниях: только что создана, принята в работу и завершена.
В первом и втором состояниях у задачи есть
Примечание. Вместо
Но есть проблема — в TypeScript структурная совместимость типов. Это значит, что если у первого и второго типа одинаковый набор полей, то это два одинаковых типа. Например, у нас есть 10 типов с разными названиями, но с идентичной структурой (с одинаковыми полями). Если подадим любой из 10 типов в функцию, которая принимает один из них, TypeScript не будет возражать.
В Java или C# наоборот номинальная система совместимости. Напишем те же 10 классов с одинаковыми полями и функцию, которая принимает один из них. Функция не примет остальные классы, если они не прямые наследники типа, который предназначен для функции.
Проблема структурной совместимости решается добавлением статуса в поле:
Для примера обратимся к ReasonML. Вот как этот тип выглядит там.
Те же поля, кроме того, что
Лайфхак. Для общих типов (например ваши доменные сущности) создайте файл
Рассмотрим, как это делать, на примере примитивного Redux-кода.
Перепишем код на TypeScript. Начнём с переименовывания файла — из
Типизируем
Примечание. Лайфхак подсмотрел в книге Basarat Ali Syed «TypeScript Deep Dive». Она немного устарела, но всё ещё полезна для тех, кто хочет погрузиться в TypeScript. О современном состоянии TypeScript читайте в блоге Мариуса Шульца. Он пишет о новых фичах и приемах. Также изучайте change logs, там есть не только информация об обновлениях, но и то, как их использовать.
Остались actions. Затипизируем и объявим каждый из них.
Поле
Обновим изначальный Redux-код:
Здесь есть опасность, если напишем
В этом action, type может быть вообще любой строкой, а не конкретным дискриминатором. После этого TypeScript не сможет отличить один action от другого и найти ошибки. Поэтому здесь должен быть именно строковый литерал. Более того, мы можем добавить такую конструкцию:
В подсказках будут всплывать три наших варианта.
Если же написать…
Мы всё затипизировали и получили тип сумму.
Представим гипотетическую ситуацию, когда в код не добавлена обработка ошибок.
Здесь нам поможет exhaustive checking — ещё одно полезное свойство типа суммы. Это возможность проверить, что мы обработали все возможные кейсы.
Добавим переменную после switch:
never — интересный тип:
Теперь компилятор укажет на ошибку «Type 'FailAction' is not assignable to type 'never'». У нас есть три возможных типа actions, из которых мы не обработали
Допишем обработку
Если добавить еще один action, компилятор подскажет, какие из них не обработаны. Это поможет, если хотите реагировать на все actions, а если только на выборочные, то нет.
Когда мы погружаемся глубже, то давление толщи воды над нами увеличивается. На определенной глубине выталкивающая сила и давление воды над нами уравновешиваются. Это зона «нейтральной плавучести», в которой мы находимся в состоянии невесомости. В этом состоянии мы можем обратиться к другим системам типов или языкам.
В Древнем Риме у жителей было три составляющих имени: фамилия, имя и «прозвище». Непосредственно имя — это «номен». От этого идет номинальная система типов — «именная». Она иногда бывает полезной.
Например, у нас есть такой код функции.
Конфигурация приходит по API:
Для функции мы хотим разработать (UI).
Функция уже умеет отображать данные, а если их нет, то лоадер. Осталось добавить API-вызов и засетить данные.
Но если мы запустим это код — всё упадет. В панели разработчика обнаружим, что
Но что, если об этом заранее подскажет компилятор? Тогда не придётся проходить весь цикл запуска, обнаружения ошибки, поиска причин.
Для этого в TypeScript есть хак — бренд-типы. Создадим
Но получим ошибку.
Теперь
Теперь всё хорошо — получили брендированные примитивы. Брендирование полезно, когда встречается несколько аргументов (строки, числа). У них есть определенная семантика (x,y координаты), и их нельзя путать. Удобно, когда компилятор подсказывает, что они перепутаны.
Но здесь есть две проблемы. Первая — в TypeScript нет нативных номинальных типов. Но есть надежда, что она решится, в репозитории языка идёт обсуждение этого вопроса.
Вторая проблема — «as», нет никаких гарантий, что в
Также, как и с
Всё это нужно проверять. Отсылаю вас к смарт-конструкторам и докладу Сергея Черепанова «Проектирование предметной области на TypeScript в функциональном стиле».
Когда мы проходим точку «нейтральной плавучести», вода давит всё сильнее и нас тянет вниз.
TypeScript умеет выводить типы.
Для этого в TypeScript есть
В подсказках видим
Это плохо — дискриминатор сломается, потому что здесь объектный литерал.
Когда в JavaScript мы создаем объект, он по умолчанию мутабельный. Это значит, что в объекте с полем и строкой, мы можем позже изменить строку на другую. Поэтому TypeScript расширяет конкретную строку до любой строки для мутабельных объектов.
Нам нужно как-то помочь TypeScript. Для этого есть as const.
Добавим — в подсказках сразу исчезнет string. Мы можем написать это не только напротив строки, а вообще всего литерала.
Тогда тип (и все поля) станут readonly.
Это полезно, потому что вряд ли вы будете менять мутабельность своего action. Поэтому добавим as const везде.
Весь код типизации actions сократили и добавили as const. Все остальное TypeScript понял сам.
TypeScript умеет выводить State. Он представлен объединением в коде выше тремя возможными состояниями isFetching: true, false или Task.
Используем type State = ReturnType. В подсказках TypeScript укажет, что есть циклическая зависимость.
Сокращаем.
Вывод готов.
Вывод похож на то, что у нас было изначально:
В обучении также: если слишком погрузиться в технологию и решить применять ее везде, то скорее всего столкнетесь с ошибками, причины которых не знаете. Это вызовет отторжение, и больше не захочется применять статические типы. Старайтесь оценивать свои силы.
Какое-то время понадобится на поддержку типизации. У нее не самый лучший UX — иногда она выдает совершенно непонятные ошибки, как и любые другие системы типов, как во Flow или Haskell.
Фотография с сайта One Ocean One Breath.
Михаил Башуров (saitonakamura) — Senior Frontend Engineer в компании WiseBits, фанат TypeScript и фридайвер-любитель. Аналогии изучения TypeScript и ныряния на глубину не случайны. Михаил расскажет, что такое discriminated unions, как использовать вывод типов, зачем нужна номинальная совместимость и брендирование. Задержите дыхание и погружайтесь.
Презентация и ссылки на GitHub с кодом примеров здесь.
Тип произведения
Типы суммы звучит алгебраично — попробуем разобраться, что это такое. В ReasonML они называются variant, в Haskell — tagged union, а в F# и в TypeScript — discriminated union.
Википедия даёт такое определение: «Тип суммы — это сумма типов произведений».
— Спасибо капитан!
Определение донельзя корректно, но бесполезно, поэтому давайте разбираться. Пойдем от частного и начнём с типа произведения.
Предположим, что у нас есть тип
Task
. У него есть два поля: id
и кто его создал.type Task = { id: number, whoCreated: number }
Это тип произведения — пересечение или intersection. Это значит, что мы можем записать тот же код, как каждое поле по отдельности.
type Task = { id: number } & { whoCreated: number }
В алгебре высказываний это называется логическим умножением: это операция «И» или «AND». Тип произведения — логическое произведение этих двух элементов.
Объект с набором полей можно выразить через логическое произведение.Тип произведения не ограничен полями. Представим, что мы любим Prototype и решили, что нам не хватает
leftPad
.type RichString = string & { leftPad: (toLength: number) => string }
Добавим его в
String.prototype
. Чтобы выразить в типах, возьмём string и функцию, которая принимает необходимые значения. Метод и string — это пересечение.Объединение
Объединение — union — пригодится, например, для представления типа переменной, которая задаёт ширину элемента в CSS: строки в 10 пикселей или абсолютного значения. На самом деле спецификация CSS гораздо сложнее, но для простоты давайте оставим так.
type Width = string | number
Пример сложнее. Предположим, у нас есть задача. Она может находиться в трех состояниях: только что создана, принята в работу и завершена.
type InitialTask = { id: number, whoCreated: number }
type InWorkTask = { id: number, whoCreated: number }
type FinishTask = { id: number, whoCreated: number, finishDate: Date }
type Task = InitialTask | InWorkTask | FinishedTask
В первом и втором состояниях у задачи есть
id
и кто её создал, а в третьем появляется дата завершения. Тут и появляется объединение — либо то, либо это.Примечание. Вместо
type
можно использовать interface
. Но есть нюансы. Результат union и intersection — это всегда тип. Можно выразить intersection через extends
, но union через interface
нельзя. При этом type
просто короче.interface
помогает только для слияния деклараций. Типы библиотеки всегда нужно выражать через interface
, потому что это позволит другим разработчиками расширить его своими полями. Так типизированы, например, JQuery-плагины.Совместимость типов
Но есть проблема — в TypeScript структурная совместимость типов. Это значит, что если у первого и второго типа одинаковый набор полей, то это два одинаковых типа. Например, у нас есть 10 типов с разными названиями, но с идентичной структурой (с одинаковыми полями). Если подадим любой из 10 типов в функцию, которая принимает один из них, TypeScript не будет возражать.
В Java или C# наоборот номинальная система совместимости. Напишем те же 10 классов с одинаковыми полями и функцию, которая принимает один из них. Функция не примет остальные классы, если они не прямые наследники типа, который предназначен для функции.
Проблема структурной совместимости решается добавлением статуса в поле:
enum
или string
. Но типы суммы это способ выразить код более семантично, по сравнению с тем, как это хранится в базе данных.Для примера обратимся к ReasonML. Вот как этот тип выглядит там.
type Task =
| Initial({ id: int, whoCreated: int })
| InWork({ id: int, whoCreated: int })
| Finished({
id: int,
whoCreated: int,
finshDate: Date
})
Те же поля, кроме того, что
int
вместо number
. Если приглядеться, заметим Initial
, InWork
и Finished
. Они располагаются не слева, в названии типа, а справа в определении. Это не просто строчка в названии, а часть отдельного типа, поэтому мы можем отличить первый от второго.Лайфхак. Для общих типов (например ваши доменные сущности) создайте файл
global.d.ts
и добавьте в него все типы. Они будут автоматически видны по всей кодовой базе TypeScript без необходимости явного импорта. Это удобно для миграции, потому что не надо беспокоиться о том, где поместить типы.Рассмотрим, как это делать, на примере примитивного Redux-кода.
export const taskReducer = (state, action) = > {
switch (action.type) {
case "TASK_FETCH":
return { isFetching: true }
case "TASK_SUCCESS":
return { isFetching: false, task: action.payload }
case "TASK_FAIL":
return { isFetching: false, erro: action.payload }
}
return state
}
Перепишем код на TypeScript. Начнём с переименовывания файла — из
.js
в .ts
. Включим все state-опции и опцию noImplicitAny. Эта опция находит функции, в которых не указан тип параметра, и полезна на этапе миграции.Типизируем
State
: добавим поле isFetching
, Task
(которого может не быть) и Error
.type Task = { title: string }
declare module "*.jpg" {
const url: string
export default url
}
declare module "*.png" {
const url: string
export default url
}
Примечание. Лайфхак подсмотрел в книге Basarat Ali Syed «TypeScript Deep Dive». Она немного устарела, но всё ещё полезна для тех, кто хочет погрузиться в TypeScript. О современном состоянии TypeScript читайте в блоге Мариуса Шульца. Он пишет о новых фичах и приемах. Также изучайте change logs, там есть не только информация об обновлениях, но и то, как их использовать.
Остались actions. Затипизируем и объявим каждый из них.
type FetchAction = {
type: "TASK_FETCH"
}
type SuccessAction = {
type: "TASK_SUCCESS",
payload: Task
type FailAction = {
type: "TASK_FAIL",
payload: Error
}
type Actions = FetchAction | SuccessAction | FailAction
Поле
type
у всех типов оно разное, это не просто string, а конкретная строка — строковый литерал. Это и есть тот самый дискриминатор — тег, по которому мы отличаем все кейсы. Получили discriminated union и, например, можем понять, что находится в payload.Обновим изначальный Redux-код:
export const taskReducer = (state: State, action: Actions): State = > {
switch (action.type) {
case "TASK_FETCH":
return { isFetching: true }
case "TASK_SUCCESS":
return { isFetching: false, task: action.payload }
case "TASK_FAIL":
return { isFetching: false, error: action.payload }
}
return state
}
Здесь есть опасность, если напишем
string
…type FetchAction = {
type: string
}
… то все сломается, потому что это уже не дискриминатор.В этом action, type может быть вообще любой строкой, а не конкретным дискриминатором. После этого TypeScript не сможет отличить один action от другого и найти ошибки. Поэтому здесь должен быть именно строковый литерал. Более того, мы можем добавить такую конструкцию:
type ActionType = Actions["type"]
.В подсказках будут всплывать три наших варианта.
Если же написать…
type FetchAction = {
type: string
}
… то в подсказках будет просто string
, потому что все остальные строки уже не важны.Мы всё затипизировали и получили тип сумму.
Exhaustive checking
Представим гипотетическую ситуацию, когда в код не добавлена обработка ошибок.
export const taskReducer = (state: State, action: Actions): State = > {
switch (action.type) {
case "TASK_FETCH":
return { isFetching: true }
case "TASK_SUCCESS":
return { isFetching: false, task: action.payload }
//case "TASK_FAIL":
//return { isFetching: false, error: action.payload }
}
return state
}
Здесь нам поможет exhaustive checking — ещё одно полезное свойство типа суммы. Это возможность проверить, что мы обработали все возможные кейсы.
Добавим переменную после switch:
const exhaustiveCheck: never = action
.never — интересный тип:
- если он находится в результате возврата функции, то функция никогда не завершается корректно (например всегда бросает ошибку или исполняется бесконечно);
- если это тип, то этот тип нельзя создать без каких-то дополнительных усилий.
Теперь компилятор укажет на ошибку «Type 'FailAction' is not assignable to type 'never'». У нас есть три возможных типа actions, из которых мы не обработали
"TASK_FAIL"
— это и есть FailAction
. Но к never он не «assignable».Допишем обработку
"TASK_FAIL"
, и ошибки больше не будет. Обработали "TASK_FETCH"
— вернулись, обработали "TASK_SUCCESS"
— вернулись, обработали "TASK_FAIL"
. Когда мы обработали все три функции, чем может быть action? Ничем — never
.Если добавить еще один action, компилятор подскажет, какие из них не обработаны. Это поможет, если хотите реагировать на все actions, а если только на выборочные, то нет.
Первый совет
Старайтесь.Сначала будет тяжело погружаться: прочитать о типе суммы, потом о произведениях, алгебраических типах данных и так по цепочке. Придется приложить усилия.
Когда мы погружаемся глубже, то давление толщи воды над нами увеличивается. На определенной глубине выталкивающая сила и давление воды над нами уравновешиваются. Это зона «нейтральной плавучести», в которой мы находимся в состоянии невесомости. В этом состоянии мы можем обратиться к другим системам типов или языкам.
Номинальная система типов
В Древнем Риме у жителей было три составляющих имени: фамилия, имя и «прозвище». Непосредственно имя — это «номен». От этого идет номинальная система типов — «именная». Она иногда бывает полезной.
Например, у нас есть такой код функции.
export const githubUrl = process.env.GITHUB_URL as string
export const nodeEnv = process.env.NODE_ENV as string
export const fetchStarredRepos = (
nodeEnv: string,
githubUrl: string
): Promise<GithubRepo[ ]> => {
if (nodeEnv = "production") {
// log call
}
// prettier-ignore
return fetch('$githubUrl}/users/saitonakamura/starred')
.then(r => r.json());
}
Конфигурация приходит по API:
GITHUB_URL=https://api.github.com
. Метод githubUrl
по API достает репозитории, на которые мы ставим «звездочку». А если nodeEnv = "production"
, то он логирует этот вызов, например, для метрик.Для функции мы хотим разработать (UI).
import React, { useState } from "react"
export const StarredRepos = () => {
const [starredRepos, setStarredRepos] = useState<GithubRepo[ ] | null>(null)
if (!starredRepos) return <div>Loading…</div>
return (
<ul>
{starredRepos.map(repo => (
<li key={repo.name}>repo.name}</li>
))}
</ul>
)
}
type GithubRepo = {
name: string
}
Функция уже умеет отображать данные, а если их нет, то лоадер. Осталось добавить API-вызов и засетить данные.
useEffect(() => {
fetchStarredRepos(githubUrl, nodeEnv).then(data => setStarredRepos(data))
}, [ ])
Но если мы запустим это код — всё упадет. В панели разработчика обнаружим, что
fetch
обращается по адресу '/users/saitonakamura/starred'
— githubUrl куда-то пропал. Оказывается, что все из-за странного дизайна — nodeEnv
идет первым. Конечно, может появиться соблазн все поменять, но функция может использоваться в других местах кодовой базы.Но что, если об этом заранее подскажет компилятор? Тогда не придётся проходить весь цикл запуска, обнаружения ошибки, поиска причин.
Брендирование
Для этого в TypeScript есть хак — бренд-типы. Создадим
Brand
, B
(строка), T
и два типа. Такой же тип создадим для NodeEnv
.type Brand<T, B extends string> = T & { readonly _brand: B }
type GithubUrl = Brand<string, "githubUrl">
export const githubUrl = process.env.GITHUB_URL as GithubUrl
type NodeEnv = Brand<string, "nodeEnv">
export const nodeEnv = process.env.NODE_ENV as NodeEnv
Но получим ошибку.
Теперь
githubUrl
и nodeEnv
нельзя присвоить друг другу, потому что это номинальные типы. Неказистые, но номинальные. Теперь мы не можем их поменять местами здесь — меняем в другой части кода.useEffect(() => {
fetchStarresRepos(nodeEnv, githubUrl).then(data => setStarredRepos(data))
}, [ ])
Теперь всё хорошо — получили брендированные примитивы. Брендирование полезно, когда встречается несколько аргументов (строки, числа). У них есть определенная семантика (x,y координаты), и их нельзя путать. Удобно, когда компилятор подсказывает, что они перепутаны.
Но здесь есть две проблемы. Первая — в TypeScript нет нативных номинальных типов. Но есть надежда, что она решится, в репозитории языка идёт обсуждение этого вопроса.
Вторая проблема — «as», нет никаких гарантий, что в
GITHUB_URL
не битая ссылка.Также, как и с
NODE_ENV
. Скорее всего, мы хотим не просто какую-то строку, а "production"
или "development"
.type NodeEnv = Brand<"production" | "development" | "nodeEnv">
export const nodeEnv = process.env.NODE_ENV as NodeEnv
Всё это нужно проверять. Отсылаю вас к смарт-конструкторам и докладу Сергея Черепанова «Проектирование предметной области на TypeScript в функциональном стиле».
Второй и третий советы
Будьте бдительны.Иногда останавливайтесь и смотрите вокруг: на другие языки, фреймворки, системы типов. Изучайте новые принципы и извлекайте уроки.
Когда мы проходим точку «нейтральной плавучести», вода давит всё сильнее и нас тянет вниз.
Расслабьтесь.Погружайтесь глубже и позвольте TypeScript делать свою работу.
Что умеет TypeScript
TypeScript умеет выводить типы.
export const createFetch = () => ({
type: "TASK_FETCH"
})
export const createSuccess = (task: Task) => ({
type: "TASK_SUCCESS"
payload: Task
})
export const createFail = (error: Error) => ({
type: "TASK_FAIL"
payload: error
})
type FetchAction = {
type: "TASK_FETCH",
type SuccessAction = {
type: "TASK_SUCCESS",
payload: Task
type FailAction = {
type: "TASK_FAIL",
payload: Error
}
Для этого в TypeScript есть
ReturnType
— он достает из функции возвращаемое значение: type FetchAction = ReturnType<typeof createFetch>
В нём мы передаём тип функции. Просто функцию записать мы не можем: чтобы взять тип от функции или переменной, нужно написать typeof
.В подсказках видим
type: string
.Это плохо — дискриминатор сломается, потому что здесь объектный литерал.
export const createFetch = () => ({
type: "TASK_FETCH"
})
Когда в JavaScript мы создаем объект, он по умолчанию мутабельный. Это значит, что в объекте с полем и строкой, мы можем позже изменить строку на другую. Поэтому TypeScript расширяет конкретную строку до любой строки для мутабельных объектов.
Нам нужно как-то помочь TypeScript. Для этого есть as const.
export const createFetch = ( ) => ({
type: "TASK_FETCH" as const
})
Добавим — в подсказках сразу исчезнет string. Мы можем написать это не только напротив строки, а вообще всего литерала.
export const createFetch = ( ) => ({
type: "TASK_FETCH"
} as const)
Тогда тип (и все поля) станут readonly.
type FetchAction = {
readonly type: "TASK_FETCH";
}
Это полезно, потому что вряд ли вы будете менять мутабельность своего action. Поэтому добавим as const везде.
export const createFetch = () => ({
type: "TASK_FETCH"
} as const)
export const createSuccess = (task: Task) => ({
type: "TASK_SUCCESS"
payload: Task
} as const)
export const createFail = (error: Error) => ({
type: "TASK_FAIL"
payload: error
} as const)
type Actions =
| ReturnType<typeof createFetch>
| ReturnType<typeof createSuccess>
| ReturnType<typeof createFail>
type State =
| { isFetching: true }
| { isFetching: false; task: Task }
| { isFetching: false; error: Error }
export const taskReducer = (state: State, action: Actions): State = > {
switch (action.type) {
case "TASK_FETCH":
return { isFetching: true }
case "TASK_SUCCESS":
return { isFetching: false, task: action.payload }
case "TASK_FAIL":
return { isFetching: false, error: action.payload }
}
const _exhaustiveCheck: never = action
return state
}
Весь код типизации actions сократили и добавили as const. Все остальное TypeScript понял сам.
TypeScript умеет выводить State. Он представлен объединением в коде выше тремя возможными состояниями isFetching: true, false или Task.
Используем type State = ReturnType. В подсказках TypeScript укажет, что есть циклическая зависимость.
Сокращаем.
type State = ReturnType<typeof taskReducer>
export const taskReducer = (state, action: Actions) => {
switch (action.type) {
case "TASK_FETCH":
return { isFetching: true }
case "TASK_SUCCESS":
return { isFetching: false, task: action.payload }
case "TASK_FAIL":
return { isFetching: false, error: action.payload }
}
const _exhaustiveCheck: never = action
return state
}
State
перестал ругаться, но теперь он any
, потому что у нас циклическая зависимость. Типизируем аргумент.type State = ReturnType<typeof taskReducer>
export const taskReducer = (state: { isFetching: true }, action: Actions) => {
switch (action.type) {
case "TASK_FETCH":
return { isFetching: true }
case "TASK_SUCCESS":
return { isFetching: false, task: action.payload }
case "TASK_FAIL":
return { isFetching: false, error: action.payload }
}
const _exhaustiveCheck: never = action
return state
}
Вывод готов.
Вывод похож на то, что у нас было изначально:
true
, false
, Task
. Здесь есть мусорные поля Error
, но с типом undefined
— поле вроде есть, а вроде и нет.Четвертый совет
Не перетруждайтесь.Если расслабиться и нырнуть слишком глубоко, кислорода может не хватить, чтобы вернуться обратно.
В обучении также: если слишком погрузиться в технологию и решить применять ее везде, то скорее всего столкнетесь с ошибками, причины которых не знаете. Это вызовет отторжение, и больше не захочется применять статические типы. Старайтесь оценивать свои силы.
Насколько TypeScript замедляет разработку
Какое-то время понадобится на поддержку типизации. У нее не самый лучший UX — иногда она выдает совершенно непонятные ошибки, как и любые другие системы типов, как во Flow или Haskell.
Чем выразительнее система, тем сложнее ошибки.Ценность системы типов в том, что она выдаёт быструю обратную связь при возникновении ошибок. Система покажет ошибки, и уйдет меньше времени на их поиск и исправление. Если тратить меньше времени на исправление ошибок, то больше внимания получат архитектурные решения. Типы не замедляют разработку, если научиться с ними работать.
Снова скоро будем погружаться в новые концепции и нырять поглубже в известные технологии на РИТ++. Обновленный онлайн-фестиваль для тех, кто делает Интернет, будет состоять из серии расширяющих кругозор докладов (25 и 26 мая) и мастер-классов для глубокого разбора практических задач (27 мая — 10 июня).
Цены на билеты снижены настолько (5900 для физлица), что РИТ++ стал не только максимально удобным и быстрым способом узнать срез всей IT-индустрии, но и очень выгодным.
А еще при поддержке AvitoTech видеозаписи FrontendConf 2019 теперь в открытом доступе. Смотрите их на youtube-канале и подписывайтесь на рассылку или telegram @FrontendConfChannel, чтобы в будущем не пропустить такие новости.
faiwer
Небольшое предостережение. Несмотря на то, что эта возможность очень мощная, используйте её аккуратно. Т.к. кол-во проивзодимых типов растёт в геометрической прогрессии. Я очень быстро это понял когда попытался скрестить типы для props у одного хитрого react-компонента с базовыми props для html-элементов. TypeScript завис, отъел около 4 GiB и тут же умер. Перезапуск, разумеется не помог. Пришлось поумерить аппетиты. Оно видимо там внутри вычисляется не ленивым образом. Мне повезло и оно упало сразу, т.к. пример явно вырожденный. Но я мог наткнуться на это в более приземлённых примерах и просто шаг за шагом убивать DX в ноль и даже не догадываться в чём дело.
Ещё 1 момент про эту возможность. Так можно определять разные сигнатуры для callback-ов в зависимости от какого-нибудь другого аргумента. Но… Доказать typescript-у очевидные вещи уже внутри метода не получится, т.к. сужение типа далеко не всемогущее. Т.е. по сути подобные финты приводят к "невозможным типам" с
&
. Ноas whatever
конечно выручает.