Pull to refresh

Comments 47

Аннотация возвращаемого значения функции - спорный холиварный поинт. Лично мне с ней удобнее: тайпчек делается на месте, в описании функции. Иначе проверка переезжает в места использования функции, и вместо одной TS-ошибки выскочит 99 ошибок в разных файлах.

Реактовские компоненты лучше объявлять так:

const Person: React.FC<PersonProps> = (props) => ...

Здесь сразу проверка и аргументов, и возвращаемого значения. Ну и другие плюшки.

И не возникает вопросов, как делать пример с коллекцией:

collection: Collection<React.FC<PersonProps>>

Вот да, я тоже не понял. Функция без возвращаемого типа выглядит диковато, и это вот "будет больше кода" конкретно в приведенном примере вообще не убеждает.

Реактовские компоненты лучше объявлять так:

Когда у вас компонент обрабатывает children и вот так, если нет:

const Person: React.VFC = (props) => ...

Вся соль здесь:

type FC<P = {}> = FunctionComponent<P>;

interface FunctionComponent<P = {}> {
  // Вот тут - PropsWithChildren
  (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null
  ...
}

type VFC<P = {}> = VoidFunctionComponent<P>;

interface VoidFunctionComponent<P = {}> {
  // А тут - только те props, которые вы указали
  (props: P, context?: any): ReactElement<any, any> | null
  ...
]

В последнее время я прихожу к тому, что лучше всегда опираться на VFC и явно указывать, где необходимо передавать children

если ничего не путаю, вроде как не рекомендуется использовать React.FC

Прикольный PR. С настройками eslinta, по которым требуется явно указать возвращаемое значение, придется либо дописать ": JSX.Element", либо таки вернуть ": React.FC". Выберу последнее.

Довод про генерики весомый, но это редкий кейс, и ничего страшного, если там придется написать чуть по-другому. Тем более что те же стрелочние функции-генерики в TSX не прокатят, то есть уже отличие от стандартного написания.

А можно просто флаг поставить. Stict=false

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

async function fetchApi(path: string) {
  const response = await fetch(`https://example.com/api${path}`)
  return response.json();
}

Функция выше может возвращать разные типы данных, в зависимости от path. Как раз для таких случаев можно смело переходить на Generic:

type User = {
  name: string;
}
async function fetchApi<ResultType>(path: string): Promise<ResultType> {
  const response = await fetch(`https://example.com/api${path}`);
  return response.json();
}
const data = await fetchApi<User[]>('/users')

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

Пришёл из PHP и JS, примерно понял, как работают обобщённые типы, но не понимаю, как писать, чтобы они были правильные. Это как с английским: читать могу, но когда сам пишу, то даже не могу правильно выбрать между can, could и would.

Кроме того, я путаюсь, где объявление типа, а где уже его использование. В вашем примере я начал искать, где описан тип, в который подставляется ResultType, потому что привык, что в учебниках сперва опишут в терминах T и P, а потом подставляют, чтобы вышло что-то типа Promise<number>, а сам Promise<T> лежит где-то ещё. В общем, очень сложно уложить всё в голове. А a<b<c>> я вообще пропускаю, не пытаясь понять, просто верю, что эта магия работает, сам я такое не напишу. Мне сама концепция не понятна, как понимать эти угловые скобки. Может быть, это как вызов функции с параметром, и тогда b<c> как бы возвращает некоторый тип, который идёт параметром в a<>. Или всё хуже?

А вообще, я сразу понял, что мне попался плохой учебник, но так как он был платный, я заставил себя его прочитать. Теперь попробую другой.

Все проще! Для себя я это объясняю так: в угловых скобках ПРИ ВЫЗОВЕ указывается тип данных с которым функция будет работать. А в ОБЪЯВЛЕНИИ функции в угловых скобках стоит Generic, обобщенный тип, который принимает тип из ВЫЗОВА. Более сложные конструкции a<b<c>>() – просто описывают "как бы вложенные объекты". a – вернет объект типа b<c>, который вернет объект типа c.
Еще раз, как верно подмечено в статье, TypeScript не делает работу, он служит описанием взаимодейстия типов в вашем коде.

Я ещё при чтении учебника осознал, что это программирование типов, такое же, как написание функций, только используются не переменные, а типы, и возвращаются тоже типы.

Пример из учебника:
type Without<T, U> = T extends U ? never : T
type A = Without<boolean | number | string, boolean>
На выходе будет number | string, но если бы в книге это не расписали, я бы ни за что не догадался, что эта строчка делает такое вот. Здесь нужно понимать иерархию типов, кто чей предок, а кто потомок, чтобы понимать, кто кого extends. И почему-то частенько в описаниях пишут не T, а U extends T, но не объясняют, почему нам важны именно потомки типа. Типа, задел на будущее, чтобы функция работала с кем-то ещё?

А ещё infer, который что-то достаёт из массивов. Вернее, из типов массивов:
T extends (infer U)[] ? U : T, причём, они этот U даже в квадратных скобках не писали, он просто появляется тут.
Для меня это белиберда, я не могу осознать, что тут происходит, а это всего лишь возврат типа элементов массива.

На минимальном уровне я понял, как это использовать, могу описать функцию, которая принимает объект с парой ключей и возвращает объект с другой парой ключей или массив. Но если у меня будет стоять задача типизировать вообще всё, то я пока не знаю, с чего начать. Очень надеюсь на учебники, который ещё нашёл, а пока что читаю "Программируй & типизируй", там подход вообще другой, там вместо объявления type на каждый чих, всё время создают классы, пользуясь тем, что классы одновременно создают одноимённые типы, в результате рождаются выражения a: A = new A(), но это я примерно понимаю так, что то ли конструктор класса может вернуть что-то отличное от A (его родителя?), то ли автор не доверяет автовыведению типа TS и хочет явно писать, что A() вернёт объект типа A.
Но с классами понимать становится проще, потому что они явно друг друга наследуют или реализуют интерфейс, а не абстрактное number extends number | undefined.

Вот только классы != типы. После F# с типами в TS очень просто работать. Надо только делать поправку, что система типов гибче и навороченнее, но превращается в тыкву в рантайме. Для всякого метапрограммирования бывает неудобно:(

На самом деле в прикладном коде писать что сложнее примитивных дженериков особо и не надо. Обычно такие вещи нужны для библиотек или переиспользуемых модулей.

Вообще на всё это можно смотреть как на программирование где типы - заначения а дженерики - функции.

> И почему-то частенько в описаниях пишут не T, а U extends T, но не объясняют, почему нам важны именно потомки типа. Типа, задел на будущее, чтобы функция работала с кем-то ещё?

Если я правильно понял то речь про код такого вида:
type Foo<T extends string> = { /**/}
или
type Bar = { /**/ }
type Baz<T extends Bar> = { /**/ }
В данном случае extends выполняет роль типов в обычных функциях, ограничивае типы, которые мы можем подставить.

На счёт последнего, c extends, какая практическая польза от T extends string вместо string? Что может скрываться пот extends string? Какой-нибудь string | number подходит под это? Если да, тогда мы не сможем сделать .length у того, что имеет тип T, нам придётся проверять, кто именно там лежит.

Или я зря вообще пытаюсь представить перемененную типа T, с которой будет что-то делаться?

Тут забавная игра слов: на самом деле "extends" подразумевает возможное сужение типа, а не расширение. То есть в данном примере Т может быть либо строкой, либо типом "набор строковых значений" - type T = 'foo' | 'bar'. В любом случае, .length и прочее строковое будет присутствовать.

Нет, string | number не подойдет. Тут может быть как и просто string, так и строковый литерал, например Foo<"bar">

Вот пример использования:
Берем строку, разбиваем её на первый символ и всё остальное и собираем новую строку.

type ToHandler<Name extends string> = Name extends `${infer FirstLetter}${infer Rest}`
  ? `on${Uppercase<FirstLetter>}${Rest}Change`
  : never
type t = ToHandler<"name"> // "onNameChange"


Опять колдовство началось. Как типы могут оперировать данными и пересобирать строки? Нужна же какая-то реализация того, что написано. И как можно затипизировать регистры символов внутри строки? Это же всё string. Как TS проверит, что функция (a: string) => string реализовала именно такое преобразование, как тут написано в типах? Он же не умеет проверять логику работы. Соответственно, зачем мы вызываем ToHandler<"name">? Чтобы получить конкретный строковый литерал? Почему сразу его не записать?

Кто-то так пишет? Есть шанс встретить такое в готовом коде и без комментариев?

Я постарался придумать пример с extends string. В данном случае мы используем литеральные типы. Это как раз type s = "name". Это тип которые соответствует не любой строке, а только строке "name". Использование для типизации CSS свойств например:
type CSSVisibility = "hidden" | "visible" | .... Или в сочетании с keyof:
const user = { name: "name", age: 25 }
function Pick<T, K extends keyof T>(o: T, key: K) {
return o[key]
}
const age: number = pick(user, "age")
const name: string = pick(user, "name")
Тут мы гарантируем что на объекте будет это свойство, и получаем тип возвращаемого значения.

Или ещё пример со склейкой строк в типах из какого-нибудь UI компонента:

type VerticalAlignment = "top" | "center" | "bottom";
type HorizontalAlignment = "left" | "center" | "right";
type Alignment = `${HorizontalAlignment}-${VerticalAlignment}`


Пример с ToHandler немного надуманный, но по идее такое может использоваться в сочетании с прокси. Мы берем объект и генерируем для каждого свойства хендлер, например для подписки на изменения свойства. Соответственно такое можно встретить в какой-нибудь библиотеке.

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

То есть, onNameChange уже где-то затипизировано как литерал, это не рандомная строка?

Пример из учебника:
type Without<T, U> = T extends U? never: T
type A = Without<boolean | number | string, boolean>
На выходе будет number | string, но если бы в книге это не расписали, я бы ни за что не догадался, что эта строчка делает такое вот. Здесь нужно понимать иерархию типов, кто чей предок, а кто потомок, чтобы понимать, кто кого extends.

Тут фокус скорее не в extends, а в том что тернарный оператор на уровне типов в TS всегда сначала раскрывает любые типы-объединения, а потом уже вычисляет.


То есть (T extends boolean? never: T) при подстановке T = boolean | number | string даёт (boolean extends boolean? never: boolean) | (number extends boolean? never: number) | (string extends boolean? never: string) = never | number | string = number | string.


Соглашусь что это контринтуитивно, но другого не завезли.

Мне сама концепция не понятна, как понимать эти угловые скобки. Может быть, это как вызов функции с параметром, и тогда b<c> как бы возвращает некоторый тип, который идёт параметром в a<>. Или всё хуже?

Именно так и есть. Дженерики являются простейшим случаем HKT (типов высшего рода), которые суть функции над типами.


В частности, в вашем примере (function fetchApi<ResultType>(path: string): Promise<ResultType>) функция fetchApi как бы принимает два параметра — первый это тип ResultType, второй это строка path.

Пример не очень полезный, прямо скажем. response.json() возвращает any, и здесь без проверки оно приводится к типу. С тем же успехом можно было написать не fetchApi<User[]>('/users'), а response.json() as User[].

По хорошему, входящие данные надо бы проверять этим или этим, и только потом приводить к типу. Без проверки это дыра в типизации.

Да, конечно, мы не говорим о полноценном приложении, хотя и в данном случае все окей, мы ожидаем в результате Promise<ResultType>, где ResultType это User[], то есть дыры в типизации конечно нет. Там еще много условностей вроде ошибок и некорректных ответов api. Но это упрощение для примера

Дыра есть, и ещё какая! Никто не мешает перепутать пути от двух конечных точек:


const data = await fetchApi<User[]>('/posts')

Для надёжной типизации API нужно где-нибудь зафиксировать путь вместе с возвращаемым типом:


async function fetchApi(path: string): Promise<unknown> {
  const response = await fetch(`https://example.com/api${path}`);
  return response.json();
}

const fetchUsers = () => fetchApi(`/users`) as Promise<User[]>;
const fetchUser = (id: string) => fetchApi(`/users/${id}`) as Promise<User>;
const fetchPosts = () => fetchApi(`/posts`) as Promise<Post[]>;
const fetchPost = (id: string) => fetchApi(`/post/${id}`) as Promise<Post>;

Ещё лучше сюда добавить валидацию, но начинать надо именно с той или иной формы сопоставления конечной точки и типа ожидаемых от неё данных.

Валидация контракта в real-time - это же совершенно другая задача.

Если у вас уже есть большой проект на JS, то разом перевести его на TS будет проблематично. Лично я решаю задачу с нескольких сторон:

  1. Если что-то можно выделить из проекта в пакет, то создайте новый пакет небольшой функциональностью (например с публикацией в корпоративный gitlab) на TS.

  2. Добавьте анотации к функциям и включите ts-check в vscode.

Пример анотации
    /**
     * Положить в кеш ответ на асинхронный запрос
     * @param {string} requestId - идентификатор запроса
     * @param {hl7.HL7Response} msg - сообщение ответ
     */
    async putAsyncCallRequest(requestId, msg) {
        ...
    }

Такие анотации позволяют проверять типы и в javascript проекте.

давно ли ES стал "частью" TS? может я что-то пропустил?

Если мы убираем TS, частью которого является JS, а JS остается. Противоречие.

ещё и кто-то минусует ))

В первоначальной фразе имеется в виду что JS можно воспринимать как подмножество TS - то есть JS-код можно интерперетировать как валидный TS

На мой вкус TS нужно воспринимать как своего рода "плагин-линтер", как некие "строительные леса" на период разработки, которые потом убирают. Собственно разработчики так его и позиционируют. Мало того с TS можно ещё и накосяить, чего без TS бы не было. Так что не всё так однозначно.

С тайпскриптом все просто:

Сто лет назад появилась "гениальная" джава, сферически фейшуйная дверь божков ООП на вершину мира.

Предполагалось что джава это конечное абсолютное решение для всего от кофемолки до шаттла и особенно энтерпрайза!

За невменяемые бюджеты и сроки "гора" джава-чемпионов рожала мёртвых мышей важно приговаривая что вот уж сейчас вот на этот раз уж точно!

А если работодатель нервничал, джава гуру гордо уходил в другую контору.

Но пришёл 2012 и вообще всем и окончательно стало понятно что джава это как минимум чёрная дыра для времени и денег и рынок переключился на простой и понятный а главное дающий результаты - JS.

Гуру так и не поняли что ООП в прикладной разработке это как прокладка рельсов до магазина под домом, каждому каждый раз будет не совсем удобно и рельсы надо полностью перекладывать.

Но джава ты успели поднатореть в риторике и привыкнуть к дурным деньгам и попыжившись с javafx, дюкскрипт и т. П. Ужасами тихо похоронили свою джаву плавно переползая на не знавшее паразитов тело js.

Тайпскрипт это кусок ООП-говна шлепнутый вчерашним джавистами, анатомически не могучим осилить прикладное функционал не программирование и уверовавшими в библию паттернов, которая писалась сишниками!

У которых есть реальная беда с типами из-за утечек памяти и безопасностью связанной с указателями.

ООП в джаве это академическая ближе, эпохи первой волны тщеславия кодеров, когда топы из SUN пришли к сопляку Гослингу на поклон с предложением: сделай нам рай, как твоец душе угодно и за любые деньги!

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

Тайпскрипт не пригоден ни для чего, кроме как играть роль прослойки для ООПнутых на всю голову в мир JS и как и в случае с джавоц вы не найдёте ни одного примера чегото действительно написанного и работающего на ТС фактически а не номинально.

Так что вывод один : правильное изучение ТС это забыть и никогда не связываться с ТС!

вы не найдёте ни одного примера чегото действительно написанного и работающего на ТС фактически а не номинально.

Не могу понять, все это жирный троллинг или реальное мнение?

Хех, фронтендер детектед)

Появится у вас проект на сотню-другую тысяч строк кода. Потом ещё пять. Это надо связать, появится какая-то Кафка. И вот внезапно вы уже забываете, что там вы вначале писали.

Адекватная ООП архитектура поможет вам разобраться как оно организовано. Статический контроль типов а) не даст поломать существующий код б) IDE поможет вам сориентироваться что и куда приходит в ваши методы.

В общем, прелести ООП и статической типизации проявляются на больших проектах. Для условного лендинга конечно хватит jquery и bootstrap.

Автор комментария имел в виду немного другое.

Я, например, и сам использую NodeJS с 2011 г. На момент, когда я делал первые шаги node, на работе занимался web-сервисами на основе J2EE. Инструменты были Tomcat, проприетарные версии JDK и глючащие только появившиеся OpenJDK. Еще до аннотаций код на JAVA6 был похож на доширак с тридцатиэтажными catch-блоками.

Кода было много. Мегабайты кода. Если бы не встроенные в IDE генераторы, бесконечное описание всех обработок этих exception-ов занимали бы вечность.

Очень сложно передать тот восторг, когда заходишь на главную страницу NodeJS, а там красуется код web-сервера всего в 6 строчек. Все логично и понятно. - Ничего лишнего! Как уравнение после того, как из него вычеркнули все общие множители, упростив до минимально-возможной длины формулы. Вот за это я полюбил JavaScript.

Да, JS совсем не идеален. И TS исправляет много косяков JS, причина которых "ну, просто так сложилось исторически". Но он, как бы, индустриализирует процесс написания кода. Безусловно, TS можно рассматривать как результат эволюции идей JS.

Иногда мне кажется, что все тогдашние поклонники JS, переболев им, ушли в rust.

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

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

Если вы работаете в команде, то не стоит так делать. При переводе проекта на typescript лучше сразу указывать корректные типы(даже если для этого требуется много времени). Сталкивался с тем, что временно указав в коде any, разработчик, потом его просто забывает исправить.

Вы слишком категоричны. Техдолг - это нормальный практичный инструмент. Да, его нужно отслуживать: всему есть цена. Но до этого нужно строить процессы.

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

Я согласен, что многое зависит от возможностей команды, просто у нас при переходе на typescript получилось слишком много кода с any. В результате очень сложно поддерживать код, который был написан другим разработчиком, даже исправление казалось бы простых багов в таком коде бывает занимает значительно больше времени, чем ожидалось.

Для этих мест с any, по факту, ничего не изменилось: был js, остался js. Сложность багов не выросла, но изменилось отношение к ним.

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

Если речь идёт именно о переписывании на TS — то вы правы, any позволяет переписывать код по частям. А вот при написании нового кода вместо any лучше бы использовать unknown.

Очень рекомендую писать возвращаемый тип из функции.

Обожглись на казалось бы простом коде:

function f(a:A,b:B){return a&&b;}

JS и TS конечно дают возвращаемый тип не boolean, а тип объекта a или b.

В итоге, вышло боком, когда подразумевали булевое значение, а получили объект :)

а можно пожалуйста получить оригинал картинки из обложки статьи, без надписи.

Спасибо за статью. Добавить прям нечего.

Правда и познакомился с тс всего с 2 месяца назад. На работе. Сначала ничего было не понятно, но через пару недель вдумчивого чтения кода, книги, статей как использовать тс, и кучи ошибок смог прочувствовать его. Таски начал шустро закрывать :)

Sign up to leave a comment.

Articles