Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TypeScriptの型とパフォーマンス (TSKaigi 2024)

TypeScriptの型とパフォーマンス (TSKaigi 2024)

TSKaigi 2024での発表です。

テキスト編集時の型の重さの影響、
Type Instantiationの回数が型の重さに繋がるという観点から、
Distributive Conditional Types、Template Literal Types、Generic Constraintsについてと、
それらがType Instantiationの回数に与える影響について、MUIやreact-hook-formの実例からお話しました。
また、改善方法やデバッグツールについても触れています。

https://tskaigi.org/talks/ypresto

当日Google Driveで上げたものをそのまま上げております。デバッグなどの参考リンクを拡充して、後日更新予定です。

FAQ:写真は自分で撮影したのですか?→すべて万博記念公園で撮影しました!

====

TypeScriptではそのチューリング完全な型計算能力を使って、ライブラリの利用者に高度な開発者体験を提供することができます。React、MUI、react-hook-formなどの、ジェネリクスを多用した型定義が、その最たる例です。

一方で型パズルや黒魔術などと呼ばれるこの技法は、使い方によってはエディタがフリーズするほどの負荷がかかり、開発者体験を損ねることもある諸刃の剣です。過剰な計算が発生するシチュエーションの実例を、tsserverの動作とデバッグ方法を交えて紹介します。

(タイムテーブルではTypeScriptと型のパフォーマンスという題になっていますm(_ _)m)

ypresto

May 11, 2024
Tweet

More Decks by ypresto

Other Decks in Programming

Transcript

  1. ypresto ←ϓϨετͰ͢ • Tw... X: @yuya_presto • LayerX バクラク事業部 •

    フロントエンド寄りのフルスタック • TypeScript歴7年 • 最近の趣味:写真
  2. ΤσΟλͷܕ͕ॏ͍ͱ ײͨ͜͡ͱ͸͋Γ·͔͢ʁ • 補完 • 定義へのジャンプ • エラー表示 • これらが遅い、反応しない

    • → tsserverが型の計算を頑張ってる WTDPEF UTTFSWFS ิ׬ͯͪ͠ΐ͏͍ͩ ܕܭࢉதɾɾɾ
  3. ฤूମݧ͸ॏ͍ܕͷӨڹΛड͚΍͍͢ • tscの場合は、若干重いなら影響は少ない • tsserverの場合は、若干重いでも大きな割合を占める ࣮ߦ एׯॏ͍ܕ ฤू एׯॏ͍ܕ ϑΝΠϧͷܕݕࠪ

    ࣌ؒ ฤू एׯॏ͍ܕ ϑΝΠϧͷܕݕࠪ ฤू एׯॏ͍ܕ ϑΝΠϧͷܕݕࠪ ฤू एׯॏ͍ܕ ϑΝΠϧͷܕݕ ϑΝΠϧͷܕݕࠪ ଞͷϑΝΠϧͷܕݕࠪ ଞͷϑΝΠϧͷܕݕࠪ ※ 型検査は開いているファイルが対象:タブを閉じるとマシになります एׯॏ͍ܕ
  4. What is Type Instantiationɿ ܕύϥϝʔλΛຒΊͨܕΛ࡞Δ type ABC = { a:

    number, b: number; c: number } type ABOnly = Omit<ABC, 'c'>; // ==> { a: number, b: number } // ܕνΣοΫ΍ิ׬ͳͲͰඞཁͳͱ͖ʹߦΘΕΔ
  5. OmitͷType Instantiation (1/5) type Omit<T, K> = Pick<T, Exclude<keyof T,

    K> T = { a: number, b: number; c: number } / K = 'c'
  6. OmitͷType Instantiation (2/5) type Omit<T, K> = Pick<T, Exclude<keyof T,

    K> Exclude<'a' | 'b' | 'c', K> T = { a: number, b: number; c: number } / K = 'c'
  7. OmitͷType Instantiation (3/5) type Omit<T, K> = Pick<T, Exclude<keyof T,

    K> Exclude<'a' | 'b' | 'c', K> Pick<T, 'a' | 'b'> T = { a: number, b: number; c: number } / K = 'c' // a, b, c͔ΒcΛऔΓআ͘
  8. OmitͷType Instantiation (4/5) type Omit<T, K> = Pick<T, Exclude<keyof T,

    K> Exclude<'a' | 'b' | 'c', K> Pick<T, 'a' | 'b'> T = { a: number, b: number; c: number } / K = 'c' // a, b, c͔ΒcΛऔΓআ͘ // ΦϒδΣΫτ͔Βa, b͚ͩΛಘΔ { a: number, b: number }
  9. OmitͷType Instantiation (5/5) type Omit<T, K> = Pick<T, Exclude<keyof T,

    K> Exclude<'a' | 'b' | 'c', K> Pick<T, 'a' | 'b'> T = { a: number, b: number; c: number } / K = 'c' { a: number, b: number } 1ճ 1ճ + தͰ3ճ 1ճʙ ࢀߟ஋ɿ6ճʙ
  10. Type Instantiationͷର৅ InstantiableType • T TypeParameter • T[xxx], xxx[T], or

    xxx[keyof T] IndexedAccessType • keyof T IndexType • T extends U ? X : Y Conditional Type • `...${T}...` TemplateLiteralType • Uppercase<T> StringMappingType • T extends string ? Foo<T> => Foo<T & string> SubstitutionType ※ TypeScript 5.4.5の types.ts より
  11. Distributive Conditional Types • Conditional Types • type Foo<T, U>

    = T extends U ? true : false • Distributive Conditional Types [link] • 条件部分にUnionが渡ってきた場合は分配される
  12. Conditional Union Distribution (1/2) Exclude<'a' | 'b' | 'c', 'c'>

    ͷྫ // ఆٛ type Exclude<T, U> = T extends U ? never : T; // ෼഑͕ͳ͍ͱ͖ʙ (T_T) ('a' | 'b' | 'c') extends 'c' ? never : ('a' | 'b' | 'c' ) => 'a' | 'b' | 'c' ʹͳͬͯ͠·ͬͯػೳ͠ͳ͍
  13. Conditional Union Distribution (2/2) Exclude<'a' | 'b' | 'c', 'c'>

    ͷྫ // ఆٛ type Exclude<T, U> = T extends U ? never : T; // ෼഑ʹΑ࣮ͬͯݱ͞ΕΔ | ('a' extends 'c' ? never : 'a') | ('b' extends 'c' ? never : 'b') | ('c' extends 'c' ? never : 'c') // 3ճ෼ɺϧʔϓͨ͠..! => 'a' | 'b'
  14. JS ProfilerɺtraceͰσόοά • getConditionalTypeInstantiation() が長い • 配下に大量の関数呼び出し → ループしてそう •

    違法改造トレースで見ると • DistributiveOmit • ComponentPropsWithRef • ※ 違法改造TS:型情報やトレースポイントが増えています [link]
  15. MUIͷButtonͷܕͷ࣮૷ (1/3) ֘౰Օॴ export type OverrideProps< Buttonͷܕఆٛ, RootComponent extends React.ElementType

    > = ( & BaseProps<Buttonͷܕఆٛ> & DistributiveOmit< React.ComponentPropsWithRef<RootComponent>, keyof BaseProps<Buttonͷܕఆٛ> > );
  16. MUIͷButtonͷܕͷ࣮૷ (3/3) Buttonͷܕຊମ // Buttonͱ͍͏ؔ਺ͷܕఆٛ interface OverridableComponent<Buttonͷఆٛ> { // 1:

    <Button component="a" href="..."> ͷͱ͖͸ɺ͜ͷΦʔόʔϩʔυ (γάωνϟ) <RootComponent extends React.ElementType>( props: { component: RootComponent; // λά໊͔ίϯϙʔωϯτ } & OverrideProps<Buttonͷఆٛ, RootComponent>, ): JSX.Element | null; // 2: component="..." ͕ॻ͍ͯͳ͍࣌͸͜͜ʹདྷΔ (props: DefaultComponentProps<Buttonͷఆٛ>): JSX.Element | null; }
  17. TypeScriptͷؔ਺ܕɾΦʔόʔϩʔυղܾ (chooseOverload) (1/4) function foo(): undefined function foo(a: { foo:

    string }): number function foo<T extends { b: number }>(a: T): T function foo(a: number): number foo({ b: 1, c: 2 }) ্͔ΒॱʹධՁ (1ͭͷͱ͖΋ಉ͡ॲཧ)
  18. TypeScriptͷؔ਺ܕɾΦʔόʔϩʔυղܾ (chooseOverload) (2/4) function foo(): undefined function foo(a: { foo:

    string }): number function foo<T extends { b: number }>(a: T): T function foo(a: number): number foo({ b: 1, c: 2 }) ্͔ΒॱʹධՁ // 1 // 2 // 3 Instantiate͠ͳ͍ͱൺֱͰ͖ͳ͍
  19. TypeScriptͷؔ਺ܕɾΦʔόʔϩʔυղܾ (chooseOverload) (3/4) function foo(): undefined function foo(a: { foo:

    string }): number function foo<T extends { b: number }>(a: T): T function foo(a: number): number foo({ b: 1, c: 2 }) ্͔ΒॱʹධՁ // 1 // 2 // 3 3.1: Ҿ਺νΣοΫ΍ܕਪ࿦ͷաఔͰ T = { b: number } (a: T) → (a: { b: number })
  20. TypeScriptͷؔ਺ܕɾΦʔόʔϩʔυղܾ (chooseOverload) (4/4) function foo(): undefined function foo(a: { foo:

    string }): number function foo<T extends { b: number }>(a: T): T function foo(a: number): number foo({ b: 1, c: 2 }) ==> ฦΓ஋ܕ͸ { b: number, c: number } // 1 // 2 // 3 3.1: Ҿ਺νΣοΫ΍ܕਪ࿦ͷաఔͰ T = { b: number } (a: T) → (a: { b: number }) function foo(a: ...): { b: number, c: number } 3.2: ਪ࿦ͨ͠ T Ͱ γάωνϟΛ instantiate ֬ఆͯ͠ऴΘΓ
  21. Generic ConstraintʹΑΔInstantiate (1/2) // 1: ݩͷγάωνϟ // React.ElementType͸ɺJSX.IntrinsicElementsͱ΄΅ಉ͡ʹ178ݸ͋Δʂ <RootComponent extends

    React.ElementType>(...): ... // 2: Ҿ਺ͷ RootComponent ΛɺͰ͔͍ React.ElementType ʹஔ͖׵͑ ( props: { component: React.ElementType; } & OverrideProps<TypeMap, React.ElementType>, ): JSX.Element | null;
  22. Generic ConstraintʹΑΔInstantiate (2/2) // 1. OverrideProps ( & BaseProps<TypeMap> &

    DistributiveOmit< React.ComponentPropsWithRef<React.ElementType>, // શλάͷͦΕͧΕͷpropsͷܕͷUnion keyof BaseProps<TypeMap> > ); // 2. ComponentPropsWithRefͷதͷܕͷྫ type PropsWithoutRef<P> = // શλάͷpropsͷUnion P extends any ? ("ref" extends keyof P ? Omit<P, "ref"> : P) : P; (λά਺)ճϧʔϓ ExcludeͰkeyof Pճϧʔϓ (ωετ)
  23. O(mn) where m = λά਺ (178), n = props਺ (aλά͸281)

    → 5ສ ࣮ࡍ͸ɺComponentPropsWithRefશମͰ15ສճ΄ͲͷΑ͏Ͱͨ͠
  24. Template Literal Types type A = PathSplit<'a.b.c'> // ==> ['a',

    'b', 'c']
 
 type PathSplit<T extends string> = T extends `${infer A}.${infer B}` ? (B extends '' ? [A] : [A, ...PathSplit<B>]) : [T] type B = PathJoin<['a','b','c']> // ==> 'a.b.c' type PathJoin<T extends string[]> = T extends [infer A, ...infer Rest] ? A extends string ? Rest extends [] ? A : Rest extends string[] ? `${A}.${PathJoin<Rest>}` : never : never : "" // aͱb.cʹόϥ͢ // [a, ...] ʹ͚ͬͭ͘Δ // aͱ [b, c] ʹόϥ͢ // `a.${࢒Γ}` ʹ͚ͬͭ͘Δ
  25. Template Literal Types ͷ׆༻ྫ • react-hook-form • type Values =

    { a: { b: { c: number } } } • form.setValue('a.b.c', 123) ←パスの位置の型を取得 • hono • "/api/posts" • client.api.posts.$get() ←オブジェクトを組み立て
  26. Template Literal Types͕ՐΛਧ͍ͨྫɿ react-hook-form // Values͕େ͖͍ߏ଄ const form = useForm<Values>()

    // setValue͕ͱʹ͔͘ॏ͍ʂ form.setValue('foo.bar.baz', 123) // ·ΔͬͱऔΔ৔߹͸໰୊ͳ͠ const values = form.getValues() type Bar3 = { baz: Bar4 baz2: Bar4 baz3: Bar4 ... baz10: Bar4 } type Bar4 = { qux: number qux2: number qux3: number ... qux20: number } interface Values { foo: { bar: { baz: Bar3 }[] }[] } ↓のどでかオブジェクトは適当ですm(_ _)m
  27. ॏ͔ͬͨͷ͸ ύεʹରԠ͢ΔܕΛऔΔܕ export type PathValue<T, P extends Path<T> | ArrayPath<T>>

    = T extends any ? P extends `${infer K}.${infer R}` // Split (6ఔ౓) ? K extends keyof T ? R extends Path<T[K]> // શ݅औಘ (215ఔ౓) ? PathValue<T[K], R> : never : K extends `${ArrayKey}` ? T extends ReadonlyArray<infer V> ? PathValue<V, R & Path<V>> // 215ఔ౓ (ωετͰ͸ͳ͍) ... 6x215x2 = 2580ɻMUIͰ͸5ສͱ͔ɻ ύεͰ͋Δͱ͜ΖͷP͕େ͖͍Unionʹɾɾʁ
  28. Base ConstraintͰղܾ → औΓ͏ΔશͯͷύεͷܕͰϧʔϓ // TFiledValues͸Values͕ೖΓ·͢ export declare function useForm<TFieldValues

    extends ...>( props?: ... ): { setValue: <TFieldName extends Path<TFieldValues> = Path<TFieldValues>>( name: TFieldName, value: PathValue<TFieldValues, TFieldName> ) => void }
  29. ස౓ͷߴ͍ͯܰ͘ Φʔόʔϩʔυ (γάωνϟ) Λ্ʹ foo(1) ͷ৔߹ function foo<T extends ͲͰ͔͍ܕ>(a:

    { rarelyUsed: Complex<T> }): void function foo(a: number): void function foo(a: number): void // ͪ͜Β͚ͩ࢖͏ਓ͸ɺͲͰ͔͍ܕͰinstantiate͞ΕͣʹࡁΉ function foo<T extends ͲͰ͔͍ܕ>(a: { rarelyUsed: Complex<T> }): void function foo(a: number): void // ΤϥʔදࣔͷͨΊ࠷ޙʹ͜Ε΋࢒͢΂͖͔΋
  30. hono͸࣮ࡍʹ γάωνϟҠಈ Ͱվળ <T1>(...) <T1, T2>(...) <T1, T2, T3>(...) ...

    <T1>(path: string, ...) <T1, T2>(path: string, ...) <T1, T2, T3>(path: string, ...) ... <T1>(...) <T1>(path: string, ...) <T1, T2>(...) <T1, T2>(path: string, ...) <T1, T2, T3>(...) <T1, T2, T3>(path: string, ...) ... 他の修正と合わせて、 7倍くらい速くなった とのこと https://github.com/honojs/hono/pull/2412
  31. ؔ਺ͷܕύϥϝʔλͷࢦఆͱ࢖༻Λ෼͚Δ form.setField(path, value) 1ඵʙ import { FieldPath, FieldPathValue, FieldValues, SetValueConfig

    } from 'react-hook-form' export interface CurriedForm<TFieldValues extends FieldValues> { field: <TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>>( name: TFieldName ) => ForField<TFieldValues, TFieldName> } interface ForField<TFieldValues extends FieldValues, TFieldName extends FieldPath<TFieldValues>> { setValue: (value: FieldPathValue<TFieldValues, TFieldName>, options?: SetValueConfig) => void } form.field(path).setValue(value) 20msʙ (pathの型パラメータが定まらないまま、PathValueに渡していたのを、field()で固定してから渡るようにした)
  32. σόοάํ๏ • tsc --noEmit --skipLibCheck --generateTrace [dirName] --extendedDiagnostics [fileName] •

    tsconfig.jsonを読み込めない • Debuggerを直接接続: TS Server Debug 拡張 • 型情報を見るのが難しい • tsserverでgenerate trace • 起動時からtsserverのtraceを有効化: vscodeの設定 • 動いているtsserverでtraceを実行するvscode拡張 ←New..!!!
  33. ·ͱΊ • 若干重いだけでも編集体験に影響 • 重いとはType Instantiation回数 • Distributed Conditional Types

    • Template Literal Types • Generic Constraints • 型定義を工夫し、Instantiationを 回避する
  34. ͓΋͠Ζྫ • function Component({ sx }: { sx: SxProps })

    { // 型パラメータが違ってstructualTypeRelatedToが重い。 // SxProps<Theme>が正解。 return <Button sx={sx} /> } • TS PRした件:keyPropertyNameによるinstantiation • Unionが10個以上のときだけ、最適化のため (が、重い) • primitiveは数に含めない修正をPRしてmergeされた
  35. FAQ • プロパティ数が多いTをkeyof Tで参照するだけでは重くない? • 確かにループだが、instantiationのほうが遅い模様 • <T extends HeavyType

    = SimpleType> • 型パラメータの推論は、型を広げてから狭める形で解決する気がす る (i.e. Contextual Type)。 •
  36. Not FAQ • 引数型を T extends Base ? never :

    T にするとオーバーロード 解決どうなる?