TypeScript with Svelte
เปิดใช้ TypeScript
หัวข้อที่มีชื่อว่า “เปิดใช้ TypeScript”เพิ่ม lang="ts" ให้ script แล้วคุณก็เขียน TypeScript ได้ rune เป็น generic อยู่แล้ว state จึงถูก type โดยไม่ต้องมีพิธีรีตอง:
<script lang="ts"> let count = $state(0); // inferred number let name = $state<string>(''); // explicit let user = $state<User | null>(null);</script>compiler infer type จากค่าเริ่มต้นเมื่อทำได้ ใช้ $state<T>(...) เมื่ออยาก explicit (เช่น ค่าเริ่มต้นเป็น null แต่ type กว้างกว่านั้น)
type ให้ props
หัวข้อที่มีชื่อว่า “type ให้ props”ประกาศ interface สำหรับ props แล้ว annotate การ destructure $props() นี่คือวิธี idiomatic ของ Svelte 5 ที่มาแทน props แบบ export let ที่ type ทีละตัวใน Svelte 4:
<script lang="ts"> interface Props { label: string; count?: number; // optional onclick?: () => void; // event callbacks are just props }
let { label, count = 0, onclick }: Props = $props();</script>
<button {onclick}>{label}: {count}</button>ผู้ใช้ component จะได้ autocompletion และ type-checking ตอนใช้ และ svelte-check จะ flag prop ที่ผิดหรือขาดตรงจุดที่เรียกใช้
snippet props และ type Snippet
หัวข้อที่มีชื่อว่า “snippet props และ type Snippet”snippet ที่ส่งเป็น prop ถูก type ด้วย type Snippet จาก svelte snippet ที่รับ parameter เป็น Snippet<[ArgType]>:
<script lang="ts"> import type { Snippet } from 'svelte';
interface Props { header: Snippet; // a plain snippet row: Snippet<[item: string]>; // a snippet taking one argument children: Snippet; // the default passed-in content }
let { header, row, children }: Props = $props();</script>
{@render header()}{@render row('first')}{@render children()}generic component
หัวข้อที่มีชื่อว่า “generic component”เมื่อ type ของ component ขึ้นกับสิ่งที่ผู้ใช้ส่งเข้ามา (list ที่ type แล้ว, select แบบ generic) ให้ประกาศ type parameter ด้วย attribute generics บน script tag:
<script lang="ts" generics="T"> interface Props { items: T[]; selected: T; onselect: (item: T) => void; }
let { items, selected, onselect }: Props = $props();</script>ตอนนี้ T ไหลผ่านทั้งหมด: ส่ง items เป็น User[] แล้ว onselect จะถูก type เป็น (item: User) => void มี type Component (จาก svelte) สำหรับ type ตัวแปรที่เก็บ component ด้วย
พิสูจน์ว่า T ไหลผ่านจริง ด้วย error จริง
หัวข้อที่มีชื่อว่า “พิสูจน์ว่า T ไหลผ่านจริง ด้วย error จริง”“T ไหลผ่าน” เป็น claim ที่ตรวจสอบได้เจาะจง — ดังนั้นตรวจมันเลย นี่คือ generic component จากด้านบน และ caller สองแบบ: แบบที่ type ของ parameter ใน callback ถูกต้อง กับแบบที่ผิด
<script lang="ts" generics="T"> interface Props { items: T[]; selected: T; onselect: (item: T) => void; } let { items, selected, onselect }: Props = $props();</script><ul> {#each items as item} <li class:active={item === selected}> <button onclick={() => onselect(item)}>{JSON.stringify(item)}</button> </li> {/each}</ul><script lang="ts"> import Select from './Select.svelte'; interface User { id: number; name: string } let users: User[] = [{ id: 1, name: 'Ada' }]; let picked = $state<User>(users[0]);</script><!-- T is inferred as User: onselect is typed (item: User) => void --><Select items={users} selected={picked} onselect={(u) => { picked = u; console.log(u.name); }} /><!-- @expect-error Property 'toFixed' does not exist on type 'User'. --><script lang="ts"> import Select from './Select.svelte'; interface User { id: number; name: string } let users: User[] = [{ id: 1, name: 'Ada' }]; let picked = $state<User>(users[0]);</script><!-- BUG: treats the callback's item as a number, but T was inferred as User --><Select items={users} selected={picked} onselect={(u) => console.log(u.toFixed(2))} />output จริงของ svelte-check สำหรับ UseSelectBad.svelte — หลักฐานว่า T ถูก infer เป็น User จริง ๆ ไม่ได้ปล่อยเป็น unknown หรือยอมให้เป็นอะไรก็ได้แบบเงียบ ๆ:
src/lib/UseSelectBad.svelte:8:72Property 'toFixed' does not exist on type 'User'..toFixed เป็น method ของ number; การที่ checker ปฏิเสธมันบน u คือหลักฐานตรง ๆ ว่า type ของ u มาจาก items: User[] ไหลผ่าน T แล้วไปลงที่ parameter ของ onselect — ไม่ใช่เรื่องบังเอิญและไม่ใช่ any
flowchart LR
props["interface Props"] --> dollar["let { ... }: Props = props()"]
snip["Snippet type"] --> dollar
gen["generics attribute (T)"] --> dollar
dollar --> check["svelte-check verifies usage + markup"] Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”TypeScript ในไฟล์ .svelte ถูกตรวจสองรอบที่ต้องสอดคล้องกัน: block <script lang="ts"> เป็น TypeScript ธรรมดา (อะไรก็ตามที่ tsc จะพูดถึงมัน) และ markup ถูกตรวจโดยความเข้าใจของ svelte-check เองต่อ template syntax ของ Svelte — {expression}, prop ที่ส่งบน component tag, type ของ item ใน {#each} ที่ infer มา generics="T" คือสิ่งที่ทำให้ type parameter ตัวเดียวไหลข้ามขอบเขตนั้นได้: ประกาศครั้งเดียวบน tag <script> แล้วใช้ใน interface Props และ infer ที่ทุก call site จากสิ่งที่ caller ส่งเข้ามาจริง — พิสูจน์แล้วด้านบนว่า T ไม่ใช่แค่พิธีการ checker ไหลมันไปถึง prop ที่ห่างออกไปหลายชั้นจากจุดที่มัน infer จริง ๆ
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ได้อะไร | ประโยชน์ | ต้นทุน |
|---|---|---|
interface Props + $props() | รูปร่าง type เดียวสำหรับพื้นผิวทั้งหมดของ component ตรวจที่ทุก call site (พิสูจน์แล้วด้านบนด้วย error จริงของ prop type) | ต้องประกาศ interface เพิ่มต่อ component เทียบกับ export let ที่ไม่มี type |
generics="T" | type ของผู้ใช้ไหลผ่านอัตโนมัติ — ไม่ต้อง any ไม่ต้อง cast ต่อการใช้งาน (พิสูจน์แล้วด้านบน) | ergonomics แค่ระดับ type parameter เดียว; component ที่ generic เหนือรูปร่างที่ไม่เกี่ยวกันหลายแบบจริง ๆ ต้องดูแล interface เองมากกว่านี้ |
Snippet<[Arg]> สำหรับ typed slot content | renderer กับ body ของ snippet ใช้ parameter type เดียวกัน ตรวจโดย svelte-check | มีพิธีการมากกว่า Snippet ที่ไม่ type เล็กน้อยเมื่อเนื้อหาไม่รับ argument จริง ๆ |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “type parameter ของ generic component เป็นแค่ documentation — ไม่ได้ถูกตรวจจริง” พิสูจน์แล้วด้านบน: error จริงของ
svelte-checkในUseSelectBad.svelte(Property 'toFixed' does not exist on type 'User') จะเกิดขึ้นได้ก็ต่อเมื่อTถูก infer เป็นUserจริง ๆ จากitemsแล้วพาไปถึง type ของ parameter ในonselect - “ต้องใช้
as anyที่ไหนสักแห่งเพื่อให้ callback prop ของ generic component ใน Svelte เข้ากันได้” ไม่ใช่ในตัวอย่างที่พิสูจน์แล้วด้านบน —onselect={(u) => { picked = u; console.log(u.name); }}type-check ผ่านสะอาด โดยuถูก type เป็นUserถูกต้อง ไม่ต้อง cast เลย - “type props มีไว้แค่จับ typo ใน component ของตัวเองเท่านั้น” error ที่พิสูจน์แล้วด้านบนถูกจับที่ markup ของ caller (
UseSelectBad.svelteไม่ใช่Select.svelte) — คุณค่าที่ใหญ่ที่สุดคือการจับการใช้ผิดที่ไหนก็ตามที่ component ถูกใช้ ไม่ใช่แค่ข้างในไฟล์ของมันเอง
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เวลารีวิว typed Svelte component ให้เช็ค:
- component ที่ “generic” ประกาศ
generics="T"บน tag<script>จริงหรือเปล่า หรือแค่ใช้Tใน interfacePropsโดยไม่เคย introduce มันเลย — ซึ่ง compile ไม่ผ่าน แทนที่จะกลายเป็นanyแบบเงียบ ๆ? - prop หรือ callback parameter ถูก type เป็น
any(หรือไม่ type เลย ทำให้ infer เป็นanyโดยปริยาย) เพื่อทำให้ type error หายไปโดยเฉพาะ แทนที่จะแก้ mismatch จริงที่svelte-checkรายงานหรือเปล่า? - type
Snippetละ parameter ไว้ (Snippetเปล่า ๆ แทนSnippet<[Arg]>) ทั้งที่ snippet นั้นถูกเรียกพร้อม argument เสมอในทางปฏิบัติ — ทำให้ call site ของ{@render row(item)}ไม่ถูก type-check หรือเปล่า? - generic ที่ explicit ของ
$state<T>(...)ถูกใช้เพื่อกลบ mismatch ของค่าเริ่มต้น แทนที่จะแก้ค่านั้นหรือขยาย type อย่างซื่อตรง (เช่นT | null) หรือเปล่า?
🐛 หา bug ในโค้ดนี้
<script lang="ts" generics="T">interface Props { items: T[]; onselect: (item: any) => void;}let { items, onselect }: Props = $props();</script><ul>{#each items as item} <li><button onclick={() => onselect(item)}>{JSON.stringify(item)}</button></li>{/each}</ul>เฉลย
items เป็น generic เหนือ T ถูกต้อง แต่ parameter ของ onselect ถูก type เป็น any แทนที่จะเป็น T — ดังนั้นการเชื่อมต่อจุดเดียวที่สำคัญ (callback รับ item type เดียวกันเป๊ะกับที่ caller ใส่เข้าไปใน items) ถูกตัดขาดแบบเงียบ ๆ caller ตอนนี้ส่ง onselect={(x: number) => x.toFixed(2)} ได้แม้ items เป็น User[] และไม่มีอะไรจับเลย: any ปิดการตรวจสำหรับ parameter นั้นทั้งหมด ซึ่งทำลายจุดประสงค์ทั้งหมดของการทำให้ component เป็น generic ตั้งแต่แรก (พิสูจน์แล้วก่อนหน้าในบทนี้: ด้วย onselect: (item: T) => void ความผิดพลาดแบบเดียวกันเป็น error จริงที่ svelte-check จับได้)
วิธีแก้คือ type แบบเดียวกับที่บทนี้ใช้ตลอด: onselect: (item: T) => void — type parameter ตัวเดียวกับที่ Props ที่เหลือใช้อยู่แล้ว ไม่ใช่ escape hatch ใหม่