Typescript для Solid¶
В этом разделе мы рассмотрим Typescript и то, как он используется при создании Solid. Эта часть может показаться немного странной для Javascript-разработчиков, не знакомых с Typescript, поэтому мы постараемся максимально разложить ее по полочкам.
Solid спроектирован таким образом, чтобы его было легко использовать с TypeScript: использование стандартного JSX делает код в значительной степени понятным для TypeScript, а также предоставляет сложные встроенные типы для своего API. В этом руководстве рассмотрены некоторые полезные советы по работе с TypeScript и типизации кода Solid.
Настройка TypeScript¶
Шаблоны Solid starter templates предлагают хорошие отправные точки для tsconfig.json
.
Чтобы настроить уже существующий проект Solid Javascript на использование Typescript, следуйте этому руководству.
Самое главное, для использования TypeScript с компилятором Solid JSX необходимо настроить TypeScript так, чтобы он оставлял JSX-конструкции в покое через "jsx": "preserve"
, а также указать TypeScript, откуда берутся JSX-типы через "jsxImportSource": "solid-js"
. Таким образом, минимальный tsconfig.json
будет выглядеть следующим образом:
1 2 3 4 5 6 |
|
Если в вашей кодовой базе используется смесь JSX-типов (например, некоторые файлы - React, а другие - Solid), вы можете установить значение по умолчанию jsxImportSource
в файле tsconfig.json
для большинства вашего кода, а затем переопределить опцию jsxImportSource
в определенных файлах .tsx
с помощью следующей прагмы:
1 |
|
или
1 |
|
Для того чтобы воспользоваться последним, необходимо убедиться, что в проекте установлены react
и его созависимости, а также что проект правильно настроен для использования JSX-файлов react.
Типы API¶
Solid написан на TypeScript, поэтому все в нем типизировано. В документации API подробно описаны типы для всех вызовов API, а также приведено несколько полезных определений типов, чтобы было проще обращаться к понятиям Solid, когда нужно указать явные типы. Здесь мы рассмотрим результирующие типы при использовании нескольких основных примитивов.
Сигналы¶
createSignal<T>
параметризуется типом T
объекта, хранящегося в сигнале. Например:
1 |
|
Приведенная выше createSignal
имеет возвращаемый тип Signal<number>
, соответствующий типу, который мы ей передали. Это кортеж из геттера и сеттера, каждый из которых имеет тип generic:
1 2 |
|
В TypeScript 3.8 добавлен новый синтаксис для импорта и экспорта только типов. В import type
импортируются только декларации, которые будут использоваться для аннотаций и деклараций типов. Они будут полностью удалены после компиляции и не будут включены в выдаваемый JavaScript. Подробнее о них можно прочитать здесь
В данном случае геттер сигнала count
имеет тип Accessor<number | undefined>
. Accessor<T>
- это определение типа, предоставляемое Solid, в данном случае эквивалентное () => число | undefined
. В данном примере | undefined
добавлено потому, что мы не указали значение по умолчанию для createSignal
, поэтому значение сигнала действительно начинается как undefined
.
Установщик сигнала setCount
имеет тип Setter<number>
, что является более сложным определением типа, примерно соответствующим (value?: number | ((prev?: number) => number)) => number
, представляющим две возможности для передаваемого аргумента: вы можете вызвать setCount
либо с number
, либо с функцией, принимающей предыдущее значение (если оно было) и возвращающей number
. Заметим, что и параметр number
, и параметр number
для функции являются необязательными, поскольку начальное значение сигнала было неопределенным
.
В действительности тип Setter
сложнее, поскольку нам необходимо различать передачу функции-установщика и передачу функции в качестве значения, на которое мы хотим установить сигнал. Если при вызове setCount(value)
возникает ошибка TypeScript "Argument ... is not assignable to parameter", то попробуйте обернуть аргумент сеттера как в setCount(() => value)
, чтобы убедиться, что value
не будет вызван.
По умолчанию.
Мы можем избежать необходимости явно указывать тип сигнала при вызове createSignal
и избежать | undefined
части типа, предоставив значение по умолчанию для createSignal
:
1 2 |
|
В этом случае TypeScript считает, что типы сигналов - number
и string
соответственно. Таким образом, например, count
получает тип Accessor<number>
, а name
- тип Accessor<string>
(без | undefined
).
Контекст¶
Аналогично сигналам, функция createContext<T>
параметризуется типом T
значения контекста. Мы можем указать этот тип в явном виде:
1 2 |
|
В данном случае dataContext
имеет тип Context<Data | undefined>
, в результате чего useContext(dataContext)
будет иметь соответствующий возвращаемый тип Data | undefined
. Причина | undefined
заключается в том, что контекст может быть не указан в предках текущего компонента, и тогда useContext
возвращает undefined
.
Если вместо этого мы предоставляем значение по умолчанию для createContext
, мы избегаем | undefined
части типа, а также часто избегаем необходимости явного указания типа createContext
:
1 |
|
В данном случае TypeScript считает, что dataContext
имеет тип Context<{count: number, name: string}>
, что эквивалентно Context<Data>
(без | undefined
).
Другой распространенной схемой является определение фабричной функции, которая производит значение для контекста. Затем мы можем получить возвращаемый тип этой функции с помощью помощника типа TypeScript ReturnType
и использовать его для ввода контекста:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
В данном примере CountNameContextType
соответствует возвращаемому значению makeCountNameContext
:
1 2 3 4 |
|
а useCountNameContext
имеет тип () => CountNameContextType | undefined
.
Если вы хотите избежать возможности undefined
, вы можете утверждать, что контекст всегда предоставляется при использовании:
1 2 |
|
Это опасное предположение; безопаснее было бы действительно предоставлять аргумент по умолчанию для createContext
, чтобы контекст всегда был определен.
Типы компонентов¶
1 2 |
|
Для типизации базовой компонентной функции используйте тип Component<P>
, где P
- тип аргумента props
, который должен быть object type. Это обеспечит передачу правильно типизированных свойств в качестве атрибутов, а также то, что возвращаемое значение является чем-то, что может быть отображено Solid: JSX.Element
может быть узлом DOM, массивом JSX.Element
, функцией, возвращающей JSX.Element
, булевым числом, undefined
/null
и т.д. Приведем несколько примеров:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Если вы хотите, чтобы ваш компонент принимал дочерние элементы JSX, вы можете либо явно добавить тип для children
в P
, либо использовать тип ParentComponent
, который автоматически добавляет children? JSX.Element
. В качестве альтернативы, если вы хотите объявить свой компонент с function
вместо const
, вы можете использовать помощник ParentProps
для типа props
. Некоторые примеры:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
В последнем примере параметр props
автоматически приобретает вид props: ParentProps<{initial: number}>
, что эквивалентно props: {initial: number, children? JSX.Element}
. (Заметим, что до версии Solid 1.4 Component
был эквивалентен ParentComponent
).
Solid предоставляет еще два подтипа Component
для работы с children
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
VoidComponent
предназначен для компонентов, которые определенно не поддерживают children
. VoidComponent<P>
эквивалентен Component<P>
, когда P
не предоставляет тип для children
.
FlowComponent
предназначен для компонентов "потока управления", таких как <Show>
и <For>
в Solid. Такие компоненты обычно требуют наличия children
для того, чтобы иметь смысл, и иногда имеют специфические типы для children
, например, требуют, чтобы это была одна функция. Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Обработчики событий¶
Пространство имен JSX
предлагает набор полезных типов, в частности, для работы с HTML DOM. Все предоставляемые типы см. в определении JSX в dom-expressions.
Одним из полезных вспомогательных типов, предоставляемых пространством имен JSX
, является JSX.EventHandler<T, E>
, который представляет собой одноаргументный обработчик событий для элемента DOM типа T
и события типа E
. Вы можете использовать его для ввода любых обработчиков событий, которые вы определяете вне JSX. Например:
1 2 3 4 5 6 7 8 9 10 11 |
|
Обработчики, определенные inline внутри on___
JSX-атрибутов (со встроенными типами событий), автоматически типизируются как соответствующие JSX.EventHandler
:
1 2 3 4 5 6 7 8 |
|
Обратите внимание, что JSX.EventHandler<T>
ограничивает атрибут события currentTarget
типом T
(в примере event.currentTarget
типизирован как HTMLInputEvent
, поэтому имеет атрибут value
). Однако атрибут события target
может быть любым DOMElement
.
Это связано с тем, что currentTarget
- это элемент, к которому был прикреплен обработчик события, поэтому он имеет известный тип, а target
- это то, с чем взаимодействовал пользователь, что вызвало появление события или его перехват обработчиком события, который может быть любым элементом DOM. Исключение составляют события ввода и фокусировки, когда они прикрепляются непосредственно к элементам input
, в качестве цели
будет указан HTMLInputElement.
Атрибут ref¶
Когда мы используем атрибут ref
с переменной, мы говорим Solid, что нужно присвоить переменной элемент DOM после того, как этот элемент будет отрисован. Без TypeScript это выглядит следующим образом:
1 2 3 4 5 6 |
|
Это создает проблему при вводе этой переменной: следует ли вводить divRef
как HTMLDivElement
, даже если она будет установлена как таковая только после рендеринга? (Здесь мы предполагаем, что режим strictNullChecks
в TypeScript включен; в противном случае TypeScript игнорирует потенциально неопределенные
переменные).
Наиболее безопасная схема в TypeScript - признать, что divRef
является неопределенной
на некоторое время, и проверять ее при использовании:
1 2 3 4 5 6 7 |
|
В качестве альтернативы, поскольку мы знаем, что onMount
вызывается только после рендеринга элемента <div>
, мы могли бы использовать nonnull assertion (!
) при обращении к divRef
внутри onMount
:
1 2 3 |
|
Другая достаточно безопасная схема - опустить undefined
из типа divRef
и использовать definite assignment assertion (!
) в атрибуте ref
:
1 2 3 4 5 6 |
|
Мы должны использовать ref={divRef!}
, поскольку TypeScript предполагает, что атрибут ref
устанавливается на переменную divRef
, а значит, divRef
уже должен быть присвоен. В Solid все наоборот: divRef
присваивается атрибутом ref
. Определенное утверждение присваивания divRef!
эффективно убеждает TypeScript в том, что все происходит именно так: TypeScript поймет, что divRef
был присвоен после этой строки.
При использовании этого шаблона TypeScript будет корректно отмечать любые случайные использования ссылок внутри тела функции (до блока JSX, в котором они определяются). Однако в настоящее время TypeScript не отмечает использование потенциально неопределенных переменных внутри вложенных функций. В контексте Solid необходимо следить за тем, чтобы не использовать рефссылки внутри createMemo
, createRenderEffect
и createComputed
(до блока JSX, в котором определяются рефссылки), поскольку эти функции вызываются сразу, и рефссылки еще не будут определены (однако TypeScript не отметит это как ошибку). Напротив, в предыдущем шаблоне эти ошибки будут отловлены.
Другой распространенный, но менее безопасный способ - поместить утверждение об определенном присваивании в точку объявления переменной.
1 2 3 4 5 6 |
|
Такой подход фактически отключает проверку присваивания для этой переменной, что является простым обходным путем, но требует дополнительной осторожности. В частности, в отличие от предыдущего паттерна, он некорректно допускает преждевременное использование переменной даже вне вложенных функций.
Сужение потока управления¶
Распространенным паттерном является использование <Show>
для отображения данных только тогда, когда эти данные определены:
1 2 3 4 5 6 |
|
В этом случае TypeScript не может определить, что два вызова name()
вернут одно и то же значение и что второй вызов произойдет только в том случае, если первый вызов вернет истинное значение. Поэтому при попытке вызвать .replace()
он будет жаловаться, что name()
может быть undefined
.
Вот два варианта решения этой проблемы:
-
Вы можете вручную утверждать, что
name()
будет не-нулевым во втором вызове, используя оператор TypeScript non-null assertion operator!
:1 2 3 4 5
return ( <Show when={name()}> Hello {name()!.replace(/\s+/g, '\xa0')}! </Show> );
-
Можно использовать форму обратного вызова
<Show>
, которая передает значение свойстваwhen
, когда оно истинно:1 2 3 4 5 6 7
return ( <Show when={name()}> {(n) => ( <>Hello {n().replace(/\s+/g, '\xa0')}!</> )} </Show> );
В данном случае типизация компонента
Show
достаточно умна, чтобы сообщить TypeScript, чтоn
является истиной, поэтому не может бытьundefined
(илиnull
, илиfalse
). Помните, что форма с утверждением null будет выброшена, если к ней обратиться, когда условие уже не будет истинным.
Специальные атрибуты и директивы JSX¶
on:___
/oncapture:___
¶
При использовании пользовательских обработчиков событий через атрибуты Solid on:___
/oncapture:___
необходимо определить соответствующие типы для получаемых объектов Event
, переопределив интерфейсы CustomEvents
и CustomCaptureEvents
в пространстве имен модуля "solid-js"
JSX`, например, так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
prop:___
/attr:___
¶
Если вы используете принудительные свойства через атрибуты Solid prop:___
или пользовательские атрибуты через Solid attr:___
attributes, вы можете определить их типы в интерфейсах ExplicitProperties
и ExplicitAttributes
соответственно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
use:___
¶
Если вы определяете пользовательские директивы для атрибутов Solid use:___
, вы можете ввести их в интерфейс Directives
, например, так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Если вы импортируете директиву d
из другого файла/модуля, а d
используется только как директива use:d
, то TypeScript (точнее, babel-preset-typescript
) по умолчанию удалит импорт
d
(опасаясь, что d
- это тип, поскольку TypeScript не понимает use:d
как ссылку на d
). Обойти эту проблему можно двумя способами:
-
Использовать опцию конфигурации
babel-preset-typescript
onlyRemoveTypeImports: true
, которая не позволяет удалять любые импорты, кромеimport type ...
. Если вы используетеvite-plugin-solid
, то можете указать эту опцию черезsolidPlugin({ typescript: { onlyRemoveTypeImports: true } })
вvite.config.ts
.Обратите внимание, что эта опция может быть проблематичной, если вы не используете бдительно
export type
иimport type
во всей своей кодовой базе. -
Добавьте фальшивый доступ типа
false && d;
к каждому модулю, импортирующему директивуd
. Это не позволит TypeScript удалитьимпорт
модуляd
, и, если вы используете древовидный метод, например Terser, этот код будет опущен из вашего конечного пакета кода.Более простой фальшивый доступ
d;
также не позволит удалитьимпорт
, но, как правило, не будет подвергаться древовидности, поэтому окажется в конечном коде.