そもそもTypeScriptのUtility Typesとは何か?
TypeScript側が用意してくれている便利な型の定義です。
JavaScript等に元々備わっている組み込み関数の型版ですね。
色々あるようですので調べてみると面白いかと思います。
今回は個人的に使用頻度の高いPartial,Required,Pick,Omitについてと活用した一例を紹介したいと思います。活用例では自作の型定義も使用します。
全体で使用する型定義
type Content = {
id?:string;
title:string;
status: '完了' | '未完' | '途中';
summary?: string[];
};
Partial <T>
Tの全てのプロパティをオプショナル化してくれます。
type PartialContent = Partial<Content>
//型推論
type PartialContent = {
id?: string | undefined;
title?: string | undefined;
status?: "完了" | "未完" | "途中" | undefined;
summary?: string[] | undefined;
}
このような感じで全てに?が付くのでそれぞれのプロパティが存在しなくてもよくなります。
const myContent:PartialContent = {
title:"hogehoge";
}
titleしか存在しない場合でも怒られなくなります。
Required <T>
Partialの逆になります。全てのプロパティを必須にしてくれます。絶対欲しいねん!ってやつです。
type RequiredContent = Required<Content>
//型推論
type RequiredContent = {
id: string;
title: string;
status: '完了' | '未完' | '途中';
summary: string[];
}
Pick <T,K>
これは直感的にわかりやすい単語でした。T型の中からKを抽出した(pick up)した新しい型を作成してくれます。
type ContentTitleSummary = Pick<Content,'title' | 'summary'>
//型推論
type ContentTitleSummary = {
title: string;
summary?: string[] | undefined;
}
Omit <T,K>
この流れから察する通りPickの逆、つまりT型の中からKを除外した新しい型を作成してくれます。
type ContentExcludeTitleSummary = Omit<Content, 'title' | 'summary'>;
//型推論
type ContentExcludeTitleSummary = {
id?: string | undefined;
status: '完了' | '未完' | '途中';
}
最後に自作の型定義と組み合わせて使用した一例
狙いとしてtype Contentで定義されたsummaryの型定義 である
string[]
のみを抽出して新たな型として使用します。オプショナルも解除します。
まずはプロパティのvalueのみを抽出する自作の型を用意します。(自作と言いつつ他の方の物を拝借しています)
type ValueOf<T> = T[keyof T];
これでT型の中からvalueのみを抽出する型ができました。
次はこのTに当たる型を用意する必要があります。
使用するのは型の一部を抽出する為のPickとオプショナル化されているものを必須化するRequiredとなります。
type RequiredPick = Required<Pick<Content,'summary'>>
//型推論
type RequiredPick = {
summary: string[];
}
あとはこの型定義を組み合わせると完成となります!
type ContentSummary = ValueOf<RequiredPick>;
//型推論
type ContentSummary = string[]
//このようにも書けます
type ContentSummary = ValueOf<Required<Pick<Content, 'summary'>>>
おまけ
mapped typesなるものも存在します。詳しい説明は出来ないので省きますが、関数のような感じで型の定義(制約)ができるようです。
Partialと同じものをmapped typesで用意するとこのようになります。
type Concrete<T> = {
[P in keyof T]-?: T[P];
};
T型に入れた型のキーから?が取り除かれる。つまりPartialと同じ事が出来ています。
この辺をもっときちんと使える日が来れば型上級者になれること間違いなしです!
かなり雑だけどスプレッド構文とObjectメソッドについて
スプレッド構文
[...array]のような感じで...がついている。
これが何を意味するかというとスプレッド(展開)している。
また、元のデータを残しつつコピーのような使い方もできる。
const array1 = [1,2,3,4];
const array2 = [5,6,7];
const array3 = [...array1,...array2]; //[1,2,3,4,5,6,7]
const obj1 = {...array1} //{1,2,3,4} このように配列の中身を展開してオブジェクトにしまったりできる。
//例えばこんな2つの配列があったとする。
const uids = ["aaaa","bbb","cccc"]
const contentIds = ["ttt","ssss","zzz"]
const newContent = {...contentIds} //特に意味はないがスプレッド構文を利用してオブジェクトにする
const combinedArray = uids.map((uid) => {
const newCombinedArray = Object.values(newContent).map((content) => {
return {
uid,content
}
})
return newCombinedArray
})
//この処理も無駄だがObject.values()で中身のvalueで構成された配列を作り.mapメソッドを使用している。.mapメソッドはオブジェクトでは使えないので注意。
console.log(combinedArray);
// [
[{"aaa","ttt"},{"aaa","sss"},{"aaa","zzz"}],
[{"bbb","ttt"},{"bbb","sss"},{"bbb","zzz"}],
[{"ccc","ttt"},{"ccc","sss"},{"ccc","zzz"}],
]このような配列が出来上がる。
削除ボタンを押した時にユニークなIDを引数として、デリートイベントを実行する簡単な処理。
最初の記述
<button onClick={() => deleteFirebaseData(result.id)}>
削除
</button>
このように記述していた。
mapで展開したidを引数で受け取る関数を全ての数分生成していることになる。
出来るだけJSX内の記述量は減らしたいという目的でこの関数は外で定義したい。
そこでhandleDeleteという関数を用意することにしたが、引数を渡すにはどうしたらいいのだろう?という疑問が浮かんだ。TypeScriptの型推論を見ると
MouseEventHandler<T = Element> = (event: MouseEvent<T, globalThis.MouseEvent>) => void
このように推論されている。
確かに
const handle = (e) => e.preventDefault()
こんな感じでeという引数が存在していたなと思いだす。
どうすればいいの?
結論、要素のidとしてmapで展開したidを付与してeventに含まれるそのidを引数として渡す方法をとる。
const handleDelete: MouseEventHandler<HTMLButtonElement> = (e) => {
deleteFirebaseData(e.currentTarget.id);
};
//途中省略
<button onClick={handleDelete} id={result.id}>
削除
</button>
event.currentTargetでそのeventが発生した要素を参照できるよう。
そのことを利用している。
またこうすることで関数を都度生成することが無くなったので、パフォーマンス的にも向上するかもしれない。
クリックイベントの型定義は面倒だったり難しかったりするけれどこういった新たな発見もあるので理解しながらやってみるという事はとても大事。
まずヌメロンゲームとは
答えの数字(今回は3桁)が用意されていて自分が入力した数字によってbite(数字と桁が一致),eat(数字が一致)が表示されてそのヒントをもとに完全一致を目指すゲーム。
state管理するもの
Reactでは状態が変化するものをstateで管理するので、まずそれを考える。
- 入力された値
- Bite数
- Eat数
- 結果(これはstateで管理する必要ないかも)
早速コード
import React, { useState } from "react";
import "./styles.css";
const answer: string[] = ["4", "5", "6"];
export default function App() {
const [num, setNum] = useState<string>("");
const [biteCount, setBiteCount] = useState(0);
const [eatCount, setEatCount] = useState(0);
const [result, setResult] = useState("");
const changeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
setNum(e.target.value);
};
const clickHandler = () => {
const numArray = num.split("");
setBiteCount(0);
setEatCount(0);
numArray.forEach((elem, index) => {
if (answer.includes(elem)) {
if (answer[index] === elem) {
setBiteCount((prev) => prev + 1);
} else {
setEatCount((prev) => prev + 1);
}
}
});
setResult(`${biteCount}BITE${eatCount}EAT`);
};
return (
<div className="App">
<input onChange={changeHandler} type="number" value={num} />
<button onClick={clickHandler}>判定</button>
{result && <p>{result}</p>}
</div>
);
}
今回は答えを定数[4,5,6]にしている。
入力された値をひとつづつ区切って配列にしてforEachで回してBite,Eat数をカウントしている。
実はこのコード欠陥がある。
クリックした際に一回目のクリックで前回のカウント数を表示してしまうのだ。
つまり2回クリックしないと正常に動作しないという事。
なぜそんなことが起こるかというとクリックした際にbiteCount,eatCountを更新する処理を行っているのだが、同じ関数内でsetResultを使ってbite,eatを更新しているので、実はstateの状態は更新される前のbite,eatつまり一つ前の状態になっている。
onblurを使う
onblurとはフォーカスが外れた時に発生するイベント。
つまりinputからフォーカスが外れた時にbite,eatを更新する処理を走らせればよい。
状態が更新されるタイミングは以下の通りになる。
- inputの変更を検知した時=>numの更新
- inputからフォーカスが外れた時=>bite,eatの更新
- 判定ボタンを押した時=>resultの更新
改善後のコード
import React, { useState } from "react";
import "./styles.css";
const answer: string[] = ["4", "5", "6"];
export default function App() {
const [num, setNum] = useState<string>("");
const [biteCount, setBiteCount] = useState(0);
const [eatCount, setEatCount] = useState(0);
const [result, setResult] = useState("");
const changeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
setNum(e.target.value);
};
const blurHandler = () => {
setBiteCount(0);
setEatCount(0);
const numArray = num.split("");
numArray.forEach( (elem, index) => {
if (answer.includes(elem)) {
if (answer[index] === elem) {
setBiteCount((prev) => prev + 1);
} else {
setEatCount((prev) => prev + 1);
}
}
});
};
const clickHandler = () => {
setResult(`${biteCount}BITE${eatCount}EAT`);
};
return (
<div className="App">
<input
onChange={changeHandler}
onBlur={blurHandler}
type="number"
value={num}
/>
<button onClick={clickHandler}>判定</button>
{result && <p>{result}</p>}
</div>
);
}
こんな感じになった。onblurは意外と良いかもしれん。
結論
結果をstateで管理しなければこんな面倒なことにはならん。