TypeScriptでコードを書く時に意識していること
なんだかんだでTypeScriptを業務でも個人開発でも使うようになって3年くらいが経過しました。
TypeScriptは便利ですよね☕️
世の中的にも、もはやTypeScript以外でフロントエンドの開発を行うことが珍しいくらいの雰囲気になってきたのではないかという感じがします。
最近というかTypeScriptを書き始めてしばらくした今、書き始めた当初とは意識することが変わってきたように感じています。
そういうわけで、今、どんなことを意識しているのかを自分の整理をこめて記載しておこうと思います。
🔥🔥🔥
型を先に定義する
ここがかなり気持ちの上で変わった部分です。
TypeScriptで型を書くということは、つまるところ自分が今から記述するコードの設計図を書くということだという意識が強くなりました。
TypeScriptの型推論は非常に強力で、自分でほとんど型を定義しなくても、多くの場合勝手に型がついてくれますし、困ることは少ない気もします(実際、僕は最初、型推論に任せたい派でした)。
ですが、型をあらかじめ書くことによって、自分が今からどのようなコードを書くべきかのアウトラインがはっきりしますし、少なくとも型定義上は意図したものから外れたコードになることを防ぐことができます。
気持ちとしても、型を設計図とするのであれば、何かを作った後に設計図を作るよりも、先に設計図がある方が自然な気がしますよね。
以下はいくつかの例です。
定数はリテラル型を先に定義する
例えば、僕は以前の記事でこんな感じのUtility typesを紹介したことがありました。
type valueOf<T> = T[keyof T];
type mappedConst<T extends string> ={
[key in T]: key;
};
上記の記事に記載がありますが、valueOf
は定数のオブジェクトから文字型リテラルのユニオン型を作ることができます。
const HOGE = {
fuga: 'fuga',
piyo: 'piyo'
} as const
type Hoge = valueOf<typeof HOGE>; // 'fuga' | 'piyo'
対して、mappedConst
は文字型リテラルのユニオン型をオブジェクトのかたちに変換するものです。
type Hoge = 'fuga' | 'piyo';
const HOGE: mappedConst<Hoge> = {
fuga: 'fuga',
piyo: 'piyo'
};
(定数をオブジェクトで持つべきか論はおいて、)当初は便利ということでvalueOf
を多用していました。
ですが、最近では基本的にはvalueOf
を使うことはありません。
理由としては、どのような値があるかを明示すことは型の仕事であり、それをもとに実装に落とし込むべきなので、予め値を用意するvalueOf
のここでの使い方は、この思想に合わないと感じ始めたからです。
戻り値の型を書く
これは別言語の経験があるメンバーからの提案を受けて、採用することになりました。
当初はめんどくさいなーと思っていましたし、推論されるので必要があるのか懐疑的でしたが、今では進んで書いています。というよりはEslintで@typescript-eslint/explicit-function-return-type
のルールを有効にしています。
書くことの最大のメリットはコードが明示的になることです。
戻り値の方を書くことでコードが何を返すのが非常に明確になりますし、関数をリファクタリングする場合にも、意図しない変更を型によって防いでくれる可能性があります。
TDDではないですが、戻り値の型を最初に書いてから中のコードを書くことで、意図した戻り値になっていることを(型上は)検証しながらコードを書くことが可能です。
Partialをできる限り使わない
これです。
Partial
は便利ですよね。ですが、基本的にはあまり使わないほうがいいと思います。
例えばstoreの型がPartialだったりすると面倒なことになります。
type Item = {
name: string;
price: number;
}
type ItemStore = Partial<Item>;
const initialValue: ItemStore = {};
実用性のない例ですが、例えば上記のようなstoreだと、毎回すべてのプロパティ(name, price)があるのかないのかを確認する必要が出ます。
であれば、基本的にはすべての値が揃ってからもらう、または初期値を用意する、あるいは部分的にundefinedを許容するなどしたほうが扱いやすいです。
type ItemStore = Item | undefiend;
const initialValue: ItemStore = undefined;
こうしておけば、storeがundefinedかどうかだけ見ればOKになります。
Partialを使う時
そうはいっても便利なときもあります。
公式の例に乗っているパターンではよく使ったりします。あるオブジェクトの任意の値のみ更新できる関数を作りたいときです。
storeやstateを大きなオブジェクトの更新するような場合に結構使ったりするかもしれません。
const [item, setItem] = useState<Item>({
name: '',
price: 0
});
const updateItem = (fieldsToUpdate: Parital<Item>) =>
{
setItem(prev => ({
...prev,
...fieldsToUpdate,
}));
};
name
の更新でもprice
の更新でもこの関数だけでいけるようになるので便利です。
不要なoptionalを避ける
undefinedかもしれないという場合は何でもoptionalにしてしまいがちです。
以下のHoge
とFuga
は同じ意味ではありません。
type Hoge = {
foo: string;
bar?: string;
}
type Fuga = {
foo: string;
bar: string | undefined;
}
const getHoge = (payload: Hoge) => {};
getHoge({ foo: 'test' }); // barがなくてもOK
const getFuga= (payload: Fuga) => {};
getFuga({ foo: 'test' }); // Error!!
undefined
かもしれない値と、渡さなくても問題がない値は意味としては異なります。
たとえば、たくさんのところで使われる関数で、bar
に関しては指定してもしなくても良いよ、という場合にはoptionalの方が適していると思いますが、bar
は必ずほしい、または、必ず渡されるのであれば、optionalにしない方が適していると思います 。
ReactでoptionalなPropsは気づきにくい
例えばこんなコードがあったとして
type Props = {
foo: string;
bar?: string;
};
const Hoge: React.FC<Props> = (props) => {
return (...);
};
このコンポーネントを使うときは以下のようになりますが、このときbar
をサジェストしてくれない気がします。
const Fuga: React.FC = () => {
return <Hoge foo="test" />;
};
広範囲で使われるコンポーネントだと、あってもなくても動くというPropsをoptionalにし、「こういうのできないのかな」と思ったタイミングで定義ファイルを見に行くでいいと思うんですが、例えばこのコンポーネントが非常に限定的な場所でしか使われない場合は、いっそのことoptionalを使わないほうが渡すべきpropsがエラーになってくれるのでわかりやすい気がします。
以下、React編
適当な命名を付与できる場合はchildrenに名前をつける
Reactのchildren
、便利ですよね。
ですが、React18からはPropsの型にデフォルトでchildrenが含まれなくなり、欲しい場合は自前で用意する事になりました。
React17のReact.VFC
と同じ挙動になりました。
type Props = {
chidren: React.ReactNode;
}
const Hoge: React.FC<Props> = ({ children }) => {
return <div>{children}</div>
}
こんな感じですね。
使う場合はこんな感じ。
<Hoge>
<span>Children!!</span>
</Hoge>
ただ、こうして自分で型を定義するようになって、childrenって命名としては漠然としていて、それが何を示しているのかわかりにくくないかなと感じ始めました。
例えば、それがPanelやBoxのような中に何でも入るコンポーネントであれば、childrenでも違和感はありません。
あるいはchildrenで指定したコンポーネントがそのコンポーネントのどこに差し込まれるのかが容易に想像できる場合は問題ないと思います。
ですが、例えば
<Profile imgSrc={imgSrc}>
children
</Profile>
だとこのchildrenは一体どんな役割なのか、いくつか想像する余地がありそうです。
そういう場合は名前をつけてやったほうがわかりやすい気がします。
type ProfileProps = {
imgSrc: string;
text: React.ReactNode;
}
// 使うとき
<Profile
imgSrc={imgSrc}
text={
<span>
<strong>Hi!!</strong>
</span>
}
/>;
ちょっといい命名が浮かびませんでしたが、childrenよりはどのような文脈で表示されるかがはっきりしたと思います。
僕は最近は割とこの様に名前をつけてchildrenを使わないことが多いです。
【検討】 利用できる型を明示する
例えば、ReactだとReact.ComponentProps<T>
でTに指定したタグに付与できるattributeを全部指定することができます
type ButtonProps = React.ComponentProps<'button'>;
便利ですね。
便利なんですが、付与されるすべての属性について、本当にそのコンポーネントが面倒を見切れるのか?という不安があります。
コンポーネントを作る作業はある(複数の)ユースケースを満たすようなパーツを作成することだと思いますが、ユースケースがあるのであれば、もらえるものを絞ってしまい必要が出たタイミングで足したほうが明示的でいいのではないかという気がしています。
type ButtonProps = {
buttonType: 'primary' | 'secondary' | 'warning';
onClick: () => void;
disabled: boolean;
['aria-label']?: string;
['aria-labelledby']?: string;
}
すこし前に読んだ記事でも同様の紹介がありました。
Generally try to avoid extending from base element props
When building a component interface, I want to be very clear about the variants it allows. I don’t want to enable extending from the base for the same reason that I don’t want className or style props. The door opens for arbitrary modification.
これは例えばmuiなどを拡張したコンポーネントを作る際も同様です。
要件を満たすことできる必要なPropsのみに絞ってもらう方がそのコンポーネントで何ができるかが明確になっていいと思います。
はい
思ったより長くなりました😅
基本的なことばかりでしたが、最近思っていることを整理するのに非常に有意義でした。
なにか他にも思いついたら書きますし、もし思い当たることがあれば教えて下さい🙆♂️
Discussion
私の環境だとサジェストされてますね。
特に変わった設定はしておらず、TypeScript のバージョンも VSCode デフォルトのものです。
サジェストの意味が違っていたらすみません。
コメントありがとうございます!
本当ですね、バッチリサジェストされますね👀
僕の方ではなんか出ないんですよね🙄一度設定見直して見ようかなと思います👀
ありがとうございます!
私もこっち派です!
ただベースとなるコンポーネントのPropsは尊重したいという想いもあり、
Pick<React.ComponentProps<'button'>, onClick | 'aria-label' ...etc> & {buttonType: 'primary' | 'secondary' | 'warning'}
のように、
Pick
やOmit
使って絞るようにしてます。便利さを教授しつつも制約を加えられていいとこ取り?出来る気がするので是非使ってみてください〜
コメントありがとうございます!
たしかにどのタグに付与するのかを明示したい場合は
Pick
を使うことでより情報を付与することができますね🙆♂️本質的な指摘じゃないけど、childrenを自分で書くとタイポしそうなので、PropsWithChildrenを使っています。
コメントありがとうございます!
たしかにその方が自分で書くより確実ですね👀
使ったことなかったです!ありがとうございます🙆♂️