d.sunnyone.org
sunnyone.org

ページ

ラベル TypeScript の投稿を表示しています。 すべての投稿を表示
ラベル TypeScript の投稿を表示しています。 すべての投稿を表示

2025-01-19

linariaからPanda CSSに移行した

うちのサイトは今のところNext.jsで書いてあるのだけど、スタイリングにはlinariaを使っていた。次の作業のためにPanda CSSに置き換えたので、その記録を残しておく。

 Panda CSSの特徴

移行時のポイントの前に、気にしておくべき造りの特徴としていくつかあげておく。

object の形でのスタイル指定 

linariaではstyled.div`` やcss``の指示でCSS文字列を記述できるが、Panda CSSではjs/tsのオブジェクトの形で指定する。最近ではよくある形。

const appItemsDivStyle = css({
    display: 'grid',
    gridTemplateColumns: '1fr',
    gap: '15px',

    md: {
        gridTemplateColumns: '1fr 1fr',
    }
});

基本的にはCSSの構造そのものを書くが、Chakraのような記法を受け付けているところもある。ブレイクポイントやデザイントークンのあたり。このへんにどう乗るかは移行時に気にかけておくと良いと思う。

クラスが細かく分割される(Atomic CSSスタイル)

linariaでは基本的にはスタイル指定の部分はまとまったままで、クラス名が生成されて適用される。このようなイメージ。

.sxatkf1 {
  padding-bottom:5px;
  border-bottom:1px solid #012356;
  margin-bottom:20px;
  line-height:1.8;
  font-size:20px;
  font-weight:700
}

classへの指定はこんなかんじ。

<h2 class="mocked-styled-0 sxatkf1">

一方で、Panda CSSではもっと細かく分解される。適当に切り出すとこんな感じ。

    .md\:fs_16px {
      font-size: 16px;
}

    .md\:fs_24px {
      font-size: 24px;
}

    .md\:mt_30px {
      margin-top: 30px;
}

分解されたスタイルは、クラス名のほうで合体される。

<h2 class="pb_5px bd-b_1px_solid_{colors.accent} mb_20px lh_1.8 fs_20px fw_bold md:fs_24px">

今回は問題にならなかったが、classがひとつであることに依存したような造りにもしなっているのであれば注意が必要。

カスケードレイヤー (@layer) を使っている

Cascade Layers  を参照。Panda CSSのみのときはたいして気にならないが、組み合わせるときに注意が必要。

バンドラの外でTypeScript ASTからスタイルを抽出する

zero-runtime CSS-in-JSのひとつのやりかたとして、linariaのようにwebpackなんかのバンドラの処理に手を入れる形でスタイル出力の変換をする形があるが、Panda CSSはts-morphを使ってバンドラの外でTypeScript ASTをparseする形でスタイルを抽出・出力する。

これはviteだのturbopackだのの話に巻き込まれにくいというメリットがあるが、linariaとは異なるタイプの制限があるので、知っておく必要がある。詳しくはマニュアルのDynamic stylingを参照。

eslint pluginによる静的検証がある

公式にeslint pluginがある(@pandacss/eslint-plugin)ので、TypeScriptの型チェックが及ばない部分、例えば動的な変数や変なトークン名なんかはeslint pluginによって検証できる。

zero-runtime らしい制約をlintでチェックしてくれるのは助かる。

移行時のポイント

上記を踏まえつつ、移行時に役に立ったり、ひっかかったりしたポイントを書く。

記述はLLMでだいたい移行できた

linariaからpandacssへの移行をお願いするだけで、だいたい書き直してくれた。後述のkeyframeを移したり、tokenに書き換えたりするところはできなかったので、手動で対応した。

ChatGPTならgpt4o-miniでだいたい行けたし、ollama + phi-4でも書き換えられた(めちゃ遅いけど)。

今回はstyled指定よりcss指定のほうがよく、diffの都合その場のほうがよかったりした(classNameのほうに移そうとしてくれたりしなかったりする)ので、そのあたりは指示した。

変数参照は基本できない→tokenを使う

はまったところとして、importした変数を参照できないので、tokenに置き換える必要がある。Design Tokensを参照。

これは、eslint pluginのno-dynamic-styling (recommendedに入っている) でわかるので、潰していけば良い。

lintでなんとかなったが、panda debugも知っておくと良いかも。

resetを持っている 

pandaは自身でresetを持っている。

もともとが雑だったので、今回はpandaのresetに乗るようにしたが、 preflight オプションでオフにできそうなので、このあたりも見ると良さそう。

素のstyleはlayerに気をつける

もともとaのようにグローバルにスタイルをつけていたのだけど、移行したらそっちが勝ちすぎたので、その部分に@layerの指示を入れた。今回はCascading Layersの記述の通り、@layer resetを指定。

keyframesの定義はconfigに

keyframesはその場に定義できないので、configに移した。configに書く設定はいくつかあるので気をつける。

eslint設定はカスタマイズが必要そう

基本的には@pandacss/recommendedで良いと思うのだけど、今回は以下を足した。

    "@pandacss/no-hardcoded-color": "off",
    "@pandacss/no-unsafe-token-fn-usage": "off"

no-hardcoded-colorは移植の都合。no-unsafe-token-fn-usageは'{sizes.foo}'ってわざわざ書かなくても'foo'でいいところはそうしましょう、みたいなオプションなのだけど、{sizes.foo} みたいに書くとno-invalid-token-pathsが機能しやすいので無効にして{}やtoken()を積極的に使っていくことにした。

記述の妥当性のほかにも、好みの指定もできて、prefer-longhand-propertiesなんかは役に立ちそう。no-margin-propertiesをCSS-in-JSが持っているのはちょっと面白い。

おわりに

思ったより考えてあるなって思った。ひっかかるポイントはあるけど、もうちょっと使ってみようと思う。

2023-12-19

例でわかるTypeScript Utility Types

これはTypeScript Advent Calendar 202319日目の記事です。例とともにUtility Typesの紹介をします。

Utility Typesとは

Utility Typesは、型から型を生成するためのTypeScript組み込みの型です。ライブラリを書いているような方ならば普段から使っていると思いますが、Utility Typesが何かわからない方もPartial<Foo>やRecord<string, string>なんかは見たことがあるのではないでしょうか。

どんなものがあるか?というのはドキュメントを見てもらえれば良いので、 詳細はそちらに譲りますが、今日はどういうときに便利なのか?という視点でUtility Typesの主要なものを分類しつついくつか紹介します。

TypeScriptが提供する型: 作るもの

Record<Keys, Type>

Recordは、Keysをキーとするプロパティを持ち、そのプロパティの値の型はTypeであるという型を生成する型です。これは主にデータを格納するためのオブジェクトの型を定義するときに使います。

type FooRecord = Record<"prop1" | "prop2", string>;
// → { prop1: string, prop2: string }

上記のようなケースだとベタに定義したほうが良いのでありがたみはないですが、keyの集合がすでに定義されている場合は便利です。

interface BarData {
    prop1: string;
    prop2: number;
    prop3: Date;
}

type BarDataErrors = Record<keyof BarData, string>
//  → { prop1: string, prop2: string, prop3: string}
stringのように広い型を指定することもできます。 
type StringRecord = Record<string, string>;

もともとRecordは

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

と単純なのでこれを書いてもいいのですが、意図を明示できる上に簡潔に記述することができます。

TypeScriptが提供する型: 主にプロパティを加工するためのもの

Pick<Type, Keys> / Omit<Type, Keys>

PickはTypeのプロパティをKeysに絞った型を作ります。OmitはTypeのプロパティからKeysを外した型を作ります。ホワイトリストとブラックリストですね。

これらの使い方は幅広いのですが、既存の関数のラッパーを作るときに便利です。

例えば、ButtonというReactコンポーネントがあり、propとしてsizeを取るとき、このsizeを“large”に固定したLargeButtonというコンポーネントを作りたいようなケースを考えます。このときにLargeButtonのpropsの型をButtonからsizeプロパティを外したものとして作ると、Buttonと同じだがsizeは渡せないということを明確にすることができます。

type LargeButtonProps = Omit<ButtonProps, "size">;

function LargeButton(props: LargeButtonProps) {
    return <Button {...props} size="large" />
}

Extract<Type, Union> / Exclude<UnionType, ExcludedMembers>

ExtractはTypeからUnionに代入可能なものを取り出した型を作り、ExcludeはUnionTypeからExcludedMembersに代入可能なものを外した型を作ります。

これは、discriminated unionとあわせて使います。例えば、type: “success”のときは正常、type: “error”のときはエラーであるResultオブジェクトを考えます。

interface ResultSuccess {
    type: "success";
    data: Record<string, string>;
}

interface ResultError {
    type: "error";
    errorMessage: string;
}

// 既存ライブラリが合わさっている型だけexportしていたり
export type Result = ResultSuccess | ResultError;

このような場合に、ExtractやExcludeを使うと限定した型を得ることができます。

type SuccessType = Extract<Result, {type: "success"}>
// ResultSuccess
type NonSuccessType = Exclude<Result, {type: "success"}>
// ResultError

Partial<Type> / Required<Type>

Partialはプロパティを任意にし、Requiredはプロパティを必須にします。

Partialは例えばプロパティを部分的に受け取ってオブジェクトを更新するような関数を作る場合に使えます。

function updateFoo(foo: Foo, fields: Partial<Foo>) {
    return {...foo, ...fields };
}

もともとすべてのプロパティがrequiredとは限らないので、Partialと比べるとRequiredを素で使うケースは少ないと思いますが、他との組み合わせ、例えばPickで明確にして使う、ということもできます。

interface Foo {
    name?: string;
    age?: number;
    description?: string;
}

type Bar = Required<Pick<Foo, "name" | "age">>;

NonNullable

NonNullableはTypeからnullとundefinedを外します。

type NullableType = string | null;
type NonNullType = NonNullable<NullableType>;
// string

まあこれはわかりやすいですよね。

TypeScriptが提供する型: 関数に関するもの

関数の型に対して何かをする型は、汎用的に関数を取ってそれをどうにかする関数には不可欠なものですが、具体的な関数がわかっている場合にも以下のような使い方をすることができます。

ReturnType

ReturnTypeは関数の型から戻り値の型を得ます。

ReturnTypeは、関数はわかっているが型にアクセスできない、または定義されていないようなとき、例えばfactory的な関数でその型の明示がないようなケースのときに、その値を中継するなどのことをするために型が欲しいときに便利です。

function createFoo() {
    return {
        a: "abc",
        b: 123
    };
}

type CreateFooReturn = ReturnType<typeof createFoo>;
// { a: string, b: number }

Parameters

Parametersは、関数の引数の型を得られます。ラッパー関数を作るような場合に、既存の関数の型をそのままに引数を足したいなどのときに便利です。

function foo(a: string|number) {
    return a;
}

type FooParameters = Parameters<typeof foo>;
function wrappedFoo(a: FooParameters[0], b: string) {
    other(b);
    return foo(a);
}

Awaited

asyncな関数だとreturn typeがPromiseになりますが、そのままでは扱いにくいです。 そんなときにAwaitedを使うと剥がすことができます。

interface Foo {
    a: string;
    b?: number;
}

export async function createFoo(): Promise<Foo> {
    return {
        a: "bar",
        b: 123
    };
}

///

type RequiredFoo = Required<Awaited<ReturnType<typeof createFoo>>>;
// {a: string, b: number}

自分で作る

Utility TypesもTypeScriptで定義された型でしかないので、このような型は自分でも作ることができます。

例えば、部分的に任意にするPartialPartiallyというものを作ると以下のようになります。

// すでに定義されているHoge
interface Hoge {
    name: string;
    description: string;
    startAt: Date;
    finishedAt: Date;
}

type PartialPartially<T, K extends keyof T> = Partial<Pick<T, K>> & Pick<T, keyof Omit<T, K>>;
// 終わってない(finishしていない)かもしれない型を作る
type HogeMayUnfinished = PartialPartial<Hoge, "finishedAt">;

このように、型の操作の仕方と、型の操作そのものを分離しておくと、あとから見たときの悩み度合いが減って良いです。

おわりに

使わないに越したこともないものもありますが、どうしようもないときは知ってるか知らないかで型の扱いの難易度が変わってきます。

Utility Typeを使って、型操作を楽にしていきましょう。

2020-06-16

AWS Lambda + TypeScript の構成メモ

AWS Lambda の関数を Node.js + TypeScriptで書くとき、どのような構成にすると便利かまとめたメモ。デプロイにはAWS SAMを使うことを想定。

ディレクトリ構造

src/ts/functions/(スタック名)/(関数名).ts をwebpackのエントリポイントとして、以下のようなディレクトリ構造にする。
src/
  templates/        # CloudFormationテンプレート
  ts/
    functions/
      (スタック名)/
        (関数名).ts # エントリポイント
    cli/
      (関数).ts     # コマンドラインでの動作確認
    lib/            # 共通コード群(AWS Lambdaのランタイムに依存しない部分)
      foo/
        bar.ts
スタック名があるのは、デプロイパッケージ(zip)をスタック単位にすることを想定。デプロイツールの都合でここは調整したほうがいいかも。

コマンドラインで動作確認できるようにする

AWSのリソースを操作する系のLambda関数を実装する場合、その処理をコマンドラインで実行できるようにしておくと便利。 cli/にコードを配置し、ts-nodeを使ってpackage.jsonの"scripts"で呼び出せるようにしておく。
  "scripts": {
    "cli:my-operation": "ts-node src/ts/cli/my-operation.ts",
  }
コードとしては、yargsでコマンドライン引数を解析して、関数に渡すだけのイメージ。
$ yarn cli:my-operation -a my-arg

webpack設定

node_modulesを持っていけば動くのだが、productionのみにしたりと面倒なので、webpackする。 例としては、こんな感じ。 ポイントは、externals: ["aws-sdk"]。標準のものやLambda Layerで提供しているものはここで除外する。ネイティブモジュールは、Lambda Layerで提供すると良いと思う。minimize: falseはコンソールで読む用。どっちでもいい。

tsconfig.json

Node v12のときはtarget: es2019。moduleはcommonjs。

source-map-supportを入れる

webpackするとスタックトレースが不便になるので、source-map-supportを入れる。
yarn add -D source-map-support @types/source-map-support
して、各エントリポイントファイルの先頭で
import "source-map-support/register";
する感じで。

必要に応じてnpm moduleにしてシェア

CloudFormation templateと、ビルドしたjsをnpm moduleにしてnpm registryにpublishしておくと、同一構成を作りやすくて便利。

2020-02-12

tslint:disable を eslint-disable にするツールを雑に作った

[2021/7/8追記] tslint-to-eslint-configの作者さんから直々にコメントに対応した旨連絡頂いたので、このツールは役目を終えました。 https://github.com/sunnyone/tslint-comment-to-eslint/issues/1

TSLintからESLintに移行するには、
// tslint:disable-next-line:no-floating-promises
なんかのコメントを、
// eslint-disable-next-line @typescript-eslint/no-floating-promises
なんかに変える必要があるのだけど、ある程度機械的にできるんじゃないかと思って作った。
https://github.com/sunnyone/tslint-comment-to-eslint

使い方

使い方はこんな感じ。直接置き換えるのでgitなりできれいな状態にして実行を。
$ yarn global add tslint-comment-to-eslint
$ tslint-comment-to-eslint 'src/**/*.ts'

仕掛け

仕掛けとしては、tslint-to-eslint-config にMapが入っていたのでrequireして引いてきて、そのMapを使って正規表現置換で変えただけ。だからルール名は変わるけど、ルールの使い方が違っているとアウト(例えばmax-classes-per-fileをdisable-next-lineするには、TSLintだとそれを2回目のclassの行に使うけど、ESLintだと1行目に使う、とか)なので確認は必須。調整前にベースに使えればどうぞ。