Skip to content

Commit 1a65224

Browse files
authored
Corrected and improved docs on type definitions for Custom Elements. (#2416)
1 parent ee365b2 commit 1a65224

File tree

1 file changed

+259
-11
lines changed

1 file changed

+259
-11
lines changed

src/guide/extras/web-components.md

Lines changed: 259 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ Vue でカスタム要素をビルドする場合、要素は Vue のランタ
220220
個々の要素コンストラクタをエクスポートして、ユーザーに必要に応じてインポートさせたり、必要なタグ名で登録できる柔軟性を持たせることをおすすめします。また、すべての要素を自動的に登録する便利な関数をエクスポートすることもできます。以下は、Vue カスタム要素ライブラリのエントリーポイントの例です:
221221

222222
```js
223+
// elements.js
224+
223225
import { defineCustomElement } from 'vue'
224226
import Foo from './MyFoo.ce.vue'
225227
import Bar from './MyBar.ce.vue'
@@ -236,31 +238,277 @@ export function register() {
236238
}
237239
```
238240

239-
もし多くのコンポーネントがある場合、Vite の [glob import](https://vitejs.dev/guide/features.html#glob-import) や webpack の [`require.context`](https://webpack.js.org/guides/dependency-management/#requirecontext) のようなビルドツールの機能を利用して、ディレクトリーからすべてのコンポーネントを読み込むこともできます。
241+
コンポーネントの利用者は、Vue ファイル内の要素を使用できます
242+
243+
```vue
244+
<script setup>
245+
import { register } from 'path/to/elements.js'
246+
register()
247+
</script>
248+
249+
<template>
250+
<my-foo ...>
251+
<my-bar ...></my-bar>
252+
</my-foo>
253+
</template>
254+
```
255+
256+
または、JSX などの他のフレームワーク内の要素を、カスタム名で使用することもできます:
257+
258+
```jsx
259+
import { MyFoo, MyBar } from 'path/to/elements.js'
260+
261+
customElements.define('some-foo', MyFoo)
262+
customElements.define('some-bar', MyBar)
263+
264+
export function MyComponent() {
265+
return <>
266+
<some-foo ...>
267+
<some-bar ...></some-bar>
268+
</some-foo>
269+
</>
270+
}
271+
```
272+
273+
### Vue ベースの Web コンポーネントと TypeScript {#web-components-and-typescript}
240274

241-
### Web コンポーネント と TypeScript {#web-components-and-typescript}
275+
Vue SFC テンプレートを記述する際には、カスタム要素として定義されたものも含めて、Vue コンポーネントの[型チェック](/guide/scaling-up/tooling.html#typescript) を行うとよいでしょう。
242276

243-
もしアプリケーションやライブラリーを開発している場合、カスタム要素として定義されているものも含めて、Vue コンポーネントを[型チェック](/guide/scaling-up/tooling.html#typescript)したいかもしれません
277+
カスタム要素はネイティブ API を使用してグローバルに登録されるため、デフォルトでは Vue テンプレートで使用された際に型推論が行われません。カスタム要素として登録された Vue コンポーネントに型サポートを提供するには、Vue テンプレートおよび/または [JSX](https://www.typescriptlang.org/docs/handbook/jsx.html#intrinsic-elements)[`GlobalComponents` インターフェース](https://github.com/vuejs/language-tools/blob/master/packages/vscode-vue/README.md#usage)を使用してグローバルコンポーネントの型付けを登録します
244278

245-
カスタム要素はネイティブ API を使用してグローバルに登録されているので、デフォルトでは Vue テンプレートで使用される時に型推論が行われません。カスタム要素として登録された Vue コンポーネントの型のサポートを提供するために、Vue テンプレート、または [JSX](https://www.typescriptlang.org/docs/handbook/jsx.html#intrinsic-elements)[`GlobalComponents` インターフェース](https://github.com/vuejs/language-tools/blob/master/packages/vscode-vue/README.md#usage)を使用することでグローバルなコンポーネントの型を登録することができます:
279+
Vue で作成されたカスタム要素の型を定義する方法は次のとおりです:
246280

247281
```typescript
248282
import { defineCustomElement } from 'vue'
249283

250-
// vue SFC
251-
import CounterSFC from './src/components/counter.ce.vue'
284+
// Vue component をインポート。
285+
import SomeComponent from './src/components/SomeComponent.ce.vue'
252286

253-
// コンポーネントを Web コンポーネントに変換
254-
export const Counter = defineCustomElement(CounterSFC)
287+
// Vue component をカスタム要素クラスに変換。
288+
export const SomeElement = defineCustomElement(SomeComponent)
255289

256-
// グローバルな型を登録
290+
// 要素クラスをブラウザに登録することを忘れずに。
291+
customElements.define('some-element', SomeElement)
292+
293+
// Vue の GlobalComponents タイプに新しい要素タイプを追加します。
257294
declare module 'vue' {
258-
export interface GlobalComponents {
259-
Counter: typeof Counter
295+
interface GlobalComponents {
296+
// ここでは必ず Vue コンポーネントタイプ(SomeElement **ではなく** SomeComponent)を渡してください。
297+
// カスタム要素にはハイフンが必要です。そのため、ここではハイフン付きの要素名を使用します。
298+
'some-element': typeof SomeComponent
299+
}
300+
}
301+
```
302+
303+
## Vue ではない Web コンポーネントと TypeScript
304+
305+
Vue で構築されていないカスタム要素の SFC テンプレートで型チェックを有効にする推奨の方法をご紹介します。
306+
307+
308+
> [!Note]
309+
> この方法は、カスタム要素を作成する際に使用するフレームワークによって異なる場合があります。
310+
311+
312+
いくつかの JS プロパティとイベントが定義されたカスタム要素があり、それが `some-lib` というライブラリーに同梱されているとします:
313+
314+
315+
```ts
316+
// file: some-lib/src/SomeElement.ts
317+
318+
// 型付き JS プロパティを持つクラスを定義します。
319+
export class SomeElement extends HTMLElement {
320+
foo: number = 123
321+
bar: string = 'blah'
322+
323+
lorem: boolean = false
324+
325+
// このメソッドはテンプレート型に公開すべきではありません。
326+
someMethod() {
327+
/* ... */
260328
}
329+
330+
// ... 実装の詳細は省略 ...
331+
// ... "apple-fell" という名前のイベントをディスパッチすると想定 ...
332+
}
333+
334+
customElements.define('some-element', SomeElement)
335+
336+
// これは、フレームワークテンプレート(例えば、Vue SFCテンプレート)で
337+
// 型チェックのために選択されるSomeElementのプロパティの一覧です。
338+
// その他のプロパティは公開されません。
339+
export type SomeElementAttributes = 'foo' | 'bar'
340+
341+
// SomeElement がディスパッチするイベントの種類を定義します。
342+
export type SomeElementEvents = {
343+
'apple-fell': AppleFellEvent
344+
}
345+
346+
export class AppleFellEvent extends Event {
347+
/* ... 詳細は省略 ... */
261348
}
262349
```
263350

351+
実装の詳細は省略しますが、重要な部分は、プロパティの型とイベントの型という 2 つの型定義があることです。
352+
353+
354+
Vue でカスタム要素の型定義を簡単に登録するための型ヘルパーを作成してみましょう
355+
356+
357+
```ts
358+
// file: some-lib/src/DefineCustomElement.ts
359+
360+
// 定義する必要のある要素ごとに、このタイプのヘルパーを再利用することができます。
361+
type DefineCustomElement<
362+
ElementType extends HTMLElement,
363+
Events extends EventMap = {},
364+
SelectedAttributes extends keyof ElementType = keyof ElementType
365+
> = new () => ElementType & {
366+
// テンプレート型のチェックに公開されるプロパティを定義するには、$props を使用します。
367+
// Vue は、特に `$props` 型からプロパティ定義を読み取ります。要素のプロパティを
368+
// グローバルな HTML プロパティと Vue の特別なプロパティと組み合わせることに
369+
// 注意してください。
370+
/** @deprecated カスタム要素参照では、$props プロパティを使用しないでください。これはテンプレートプロパティ型専用です。 */
371+
$props: HTMLAttributes &
372+
Partial<Pick<ElementType, SelectedAttributes>> &
373+
PublicProps
374+
375+
// イベントタイプを明示的に定義するには、$emit を使用します。
376+
// Vue は、イベントタイプを`$emit`タイプから読み取ります。
377+
// `$emit` は、`Events` にマッピングする特定のフォーマットを必要とします。
378+
/** @deprecated カスタム要素参照では $emit プロパティを使用しないでください。これはテンプレートプロパティ型専用です。 */
379+
$emit: VueEmit<Events>
380+
}
381+
382+
type EventMap = {
383+
[event: string]: Event
384+
}
385+
386+
// これは、Vue の $emit 型が期待する形式に EventMap をマッピングします。
387+
type VueEmit<T extends EventMap> = EmitFn<{
388+
[K in keyof T]: (event: T[K]) => void
389+
}>
390+
```
391+
392+
> [!Note]
393+
> 私たちは `$props``$emit` を非推奨としました。カスタム要素の `ref` を取得した際に、これらのプロパティを使用したくなる誘惑にかられないようにするためです。これらのプロパティはカスタム要素に関しては型チェックのみに使用されるためです。これらのプロパティは実際にはカスタム要素のインスタンスには存在しません。
394+
395+
396+
397+
398+
型ヘルパーを使用して、Vue テンプレートで型チェックのために公開すべき JS プロパティを選択できます:
399+
400+
401+
```ts
402+
// file: some-lib/src/SomeElement.vue.ts
403+
404+
import {
405+
SomeElement,
406+
SomeElementAttributes,
407+
SomeElementEvents
408+
} from './SomeElement.js'
409+
import type { Component } from 'vue'
410+
import type { DefineCustomElement } from './DefineCustomElement'
411+
412+
// Vue の GlobalComponents タイプに新しい要素タイプを追加します。
413+
declare module 'vue' {
414+
interface GlobalComponents {
415+
'some-element': DefineCustomElement<
416+
SomeElement,
417+
SomeElementAttributes,
418+
SomeElementEvents
419+
>
420+
}
421+
}
422+
```
423+
424+
`some-lib` が TypeScript のソースファイルを `dist/` フォルダにビルドするとします。 `some-lib` のユーザーは、`SomeElement` をインポートし、Vue SFC で次のように使用できます:
425+
426+
427+
```vue
428+
<script setup lang="ts">
429+
// これにより、要素が作成され、ブラウザーに登録されます。
430+
import 'some-lib/dist/SomeElement.js'
431+
432+
// TypeScript と Vue を使用しているユーザーは、さらに Vue 固有の型定義を
433+
// インポートする必要があります(他のフレームワークを使用しているユーザーは、
434+
// 他のフレームワーク固有の型定義をインポートする場合があります)。
435+
import type {} from 'some-lib/dist/SomeElement.vue.js'
436+
437+
import { useTemplateRef, onMounted } from 'vue'
438+
439+
const el = useTemplateRef('el')
440+
441+
onMounted(() => {
442+
console.log(
443+
el.value!.foo,
444+
el.value!.bar,
445+
el.value!.lorem,
446+
el.value!.someMethod()
447+
)
448+
449+
// これらの props は使用しないでください。これらは `undefined` です(IDE では取り消し線が表示されます):
450+
el.$props
451+
el.$emit
452+
})
453+
</script>
454+
455+
<template>
456+
<!-- これで型チェックを行いながら要素を使用できます: -->
457+
<some-element
458+
ref="el"
459+
:foo="456"
460+
:blah="'hello'"
461+
@apple-fell="
462+
(event) => {
463+
// ここで、`event` の型は `AppleFellEvent` であると推論されます
464+
}
465+
"
466+
></some-element>
467+
</template>
468+
```
469+
470+
要素に型定義がない場合、プロパティとイベントの型はより手動で定義することができます:
471+
472+
473+
```vue
474+
<script setup lang="ts">
475+
// `some-lib` が型定義のないプレーンな JavaScript で、TypeScript が型を
476+
// 推論できないと仮定します:
477+
import { SomeElement } from 'some-lib'
478+
479+
// 前回と同じ型ヘルパーを使用します。
480+
import { DefineCustomElement } from './DefineCustomElement'
481+
482+
type SomeElementProps = { foo?: number; bar?: string }
483+
type SomeElementEvents = { 'apple-fell': AppleFellEvent }
484+
interface AppleFellEvent extends Event {
485+
/* ... */
486+
}
487+
488+
// Vue の GlobalComponents 型に新しい要素タイプを追加します。
489+
declare module 'vue' {
490+
interface GlobalComponents {
491+
'some-element': DefineCustomElement<
492+
SomeElementProps,
493+
SomeElementEvents
494+
>
495+
}
496+
}
497+
498+
// ... 以前と同じように、要素への参照を使用します ...
499+
</script>
500+
501+
<template>
502+
<!-- ... 以前と同じように、テンプレート内の要素を使用します ... -->
503+
</template>
504+
```
505+
506+
カスタム要素の作成者は、ライブラリーからフレームワーク固有のカスタム要素の型定義を自動的にエクスポートすべきではありません。例えば、ライブラリーの残りの部分もエクスポートする `index.ts` ファイルからエクスポートすべきではありません。そうしないと、ユーザーに予期しないモジュール拡張エラーが発生します。ユーザーは、必要なフレームワーク固有の型定義ファイルをインポートする必要があります。
507+
508+
509+
510+
511+
264512
## Web コンポーネント と Vue コンポーネントの比較 {#web-components-vs-vue-components}
265513

266514
開発者の中には、フレームワークに依存した独自のコンポーネントモデルは避けるべきであり、カスタム要素のみを使用することでアプリケーションの「将来性」を確保できると考える人もいます。ここでは、この考え方が問題を単純化しすぎていると思われる理由を説明します。

0 commit comments

Comments
 (0)