お知らせ

現在サイトのリニューアル作業中のため、全体的にページの表示が乱れています。
投稿日:
言語::TypeScriptジャンル::調査

TypeScriptをぼちぼちやってきた中で、型推論が素直に決まらないみたいな所が出てきたので書き置きとして残しておきます
具体的には何かしらの関数を通して処理を隠蔽すると上手くいかなくなるケースがあります

型が推論されないケース

検査関数的なものを通した場合に、後続処理で期待通り型が推論されなくなるケースです

その1

type HogePiyo = 'hoge' | 'piyo';

type InputProps<ValueT> = {
  value: ValueT;
  error: string;
};

const required = (val: string) => {
  return val === '';
};

const registerHogePiyo = (value: HogePiyo) => {
  console.log('register', value);
};

const input: InputProps<HogePiyo | ''> = {
  value: '',
  error: '',
};

if (required(input.value)) {
  input.error = 'ERR';
  console.error(input.error);
} else {
  // ここでは input.value は 'hoge' | 'piyo' となるはずだが
  // TSC は 'hoge' | 'piyo' | '' と解釈する
  registerHogePiyo(input.value);
}

その 2

type HogePiyo = 'hoge' | 'piyo';

type InputProps<ValueT> = {
  value: ValueT;
  error: string;
  hasError: boolean;
};

const required = <ValueT>(obj: InputProps<ValueT | ''>) => {
  if (obj.value === '') {
    return { value: '', error: '必須入力です', hasError: true };
  } else {
    return { value: obj.value, error: '', hasError: false };
  }
};

const registerHogePiyo = (value: HogePiyo) => {
  console.log('register', value);
};

const input: InputProps<HogePiyo | ''> = {
  value: '',
  error: '',
  hasError: false,
};

const result = required(input);
if (result.hasError) {
  console.error(result.error);
} else {
  // ここでは input.value は 'hoge' | 'piyo' となるはずだが
  // TSC は 'hoge' | 'piyo' | '' と解釈する
  registerHogePiyo(result.value);
}

export {};

型を推論されるようにしたケース

オブジェクトに型判定用のプロパティを生やしasで型を明示することでクリアしています

type HogePiyo = 'hoge' | 'piyo';

type InputProps<ValueT> = {
  value: ValueT;
  error: string;
  hasError: boolean;
};

const required = <ValueT>(obj: InputProps<ValueT | ''>) => {
  if (obj.value === '') {
    return { value: '', error: '必須入力です', hasError: true } as {
      value: '';
      error: string;
      hasError: true;
    };
  } else {
    return { value: obj.value, error: '', hasError: false } as {
      value: ValueT;
      error: string;
      hasError: false;
    };
  }
};

const registerHogePiyo = (value: HogePiyo) => {
  console.log('register', value);
};

const input: InputProps<HogePiyo | ''> = {
  value: '',
  error: '',
  hasError: false,
};

const result = required(input);
if (result.hasError) {
  console.error(input.error);
} else {
  registerHogePiyo(result.value);
}

export {};

あとがき

  • Assertion Functionsを使う方法もあると思いますが、何も考えずAssertion Functionsを書くと次のような弊害があり、積極的に採用しづらいと考えています
    • 何もしていなければ基本的にバンドル後のソースコードに出てくる
      • 要するにバンドルサイズが大きくなります
    • throwを書く必要性があり、バンドル後に残っている以上、このthrowcatchする必要性がある
      • 無意味な実装工数が生まれる
    • そもそも論としてAssertion Functionsを書く工数が生まれる
    • Assertion Functionsの振る舞いを見ている限り、asに限りなく近く、ぶっちゃけasで書いたほうが楽
  • 絶対にasを使わず現実的な工数で開発するというのは、割と非現実ではないかという感触があります
    • 個人的に型推論はIDEが勝手にやってくれるおかげで楽ができるシステムだと考えているので、型推論の補佐をする仕組みに工数をかけるというのは本末転倒な気がしています
  • 品質は大切なことですが、反面で少なくないビジネスには期限もあります
    • 品質に寄せすぎた過剰に冗長なコードも、納期に寄せすぎた脆弱性不具合山盛りスパゲティコードもどちらも考えものであり、その辺のバランスが上手くとれるやり方がなんかないかなーと考えることはしばしばあります
投稿日:
技術::Cookieジャンル::調査

各サービスの振る舞いが少し気になったので適当にサッと軽く見たメモです
ある程度モダンそうなところを適当に選んだので特に観点とかはないです

確認環境

  • Google Chrome 91.0.4472.124(Official Build)
  • Cookie offはファーストパーティCookieをブロックして確認

超簡単なまとめ

サービス Cookie off ログイン JS off
Qiita エラー 一部描画が欠損
Twitter 何も起きない エラー
GitHub エラー 一部描画が欠損
Google エラー 一部描画が欠損, 場合によってエラー
Youtube 未確認 読込中になる
mercari 未確認 読込中になる
DMM 何も起きない 一部描画が欠損, 警告が出る, 一部機能不全
DLSite ログアウトページに飛ぶ 一部描画が欠損, 場合によって例外が出る
Skeb 白紙ページ 白紙ページ
Tayori エラー 一部描画が欠損, 一部機能不全
SoundCloud 何も起きない エラー

Qiita

ログインしようとするとエラーになります

image-1656171614782.png

JS off

ページの一部が欠損しますが、特にエラーは出ない模様

Twitter

ログインしようとするとログインページでループします

image-1656171633639.png

JS off

エラーが出ます

image-1656171647796.png

GitHub

めちゃくちゃシンプルなエラーが出ます

image-1656171657401.png

JS off

ページの一部が欠損し、場所によってはエラーになる模様

image-1656171663805.png

Google

検索ページとログインページで確認

ログインしようとするとエラーになります

image-1656171670802.png

JS off

検索はできましたが、ログインしようとするとエラーになります

image-1656171677370.png

Youtube

Googleと同じなのでパス

JS off

読込中の状態で止まります

image-1656171688195.png

mercari

未確認

JS off

読込中の状態で止まります

DMM

Twitter同様にログインページでループしました

image-1656171751059.png

JS off

ページ上部に通知が生えてきます
ついでにページ表示が崩れたり、一部のページ遷移が機能しなくなりました

image-1656171759245.png

DLSite

ログインしようとするとログアウト処理に飛びました

image-1656171766632.png

カートにアイテムを入れても入りません

image-1656171772865.png

JS off

ページの一部が欠損するので成人向けサイトでも超健全に!

image-1656171779255.png

カートからアイテムを消すと謎のエラーが出ますが、カートからは一応消えます

image-1656171785967.png

skeb

白紙ページになります
Consoleに次のエラーが出るので想定外ということでしょう

DOMException: Failed to read the 'localStorage' property from 'Window': Access is denied for this document.

JS off

白紙ページになりました

Tayori

エラーになります

image-1656171796586.png

JS off

  • トップページの一部が欠損
  • トップページからヘッダーのリンクを叩くと表示されるコンテンツが白紙に
  • ユーザーのFAQページは検索が動作不能

SoundCloud

ログインできずにループします

JS off

エラーになります

image-1656171803459.png

ReactでSPAを組んでいてブラウザバックしたときのフォームの入力内容が消し飛んで気になったので、ブラウザバックした時にどうなるのかというのを軽く調査した結果

確認したパターンとしては次の2つ

  1. フォームの入力値をDOMに保持させるステートレスな方式
  2. フォームの入力値をJSに保持させるステートフルな方式

LocalStorageに画面情報を保存して復元するとか、条件分岐を使ってDOMを隠して保持させるとか、そういう系は考慮しない

結果

DOM 保持 JS 保持
SPA 内ブラウザバック 消える 保持される
SPA 外ブラウザバック 保持される 消える
コード記述量 少ない 多い

1. フォームの入力値をDOMに保持させるステートレスな方式

この方式だとhistory.push()でDOMが消し飛ぶのでSPA内でのブラウザバックで入力したデータが保持されません
その代わりSPAの外、別のサイトに遷移してからブラウザバックしたときは、DOMが残っているので入力したデータが保持されます

2. フォームの入力値をJSに保持させるステートフルな方式

この方式だとJSでステート管理をしているため、history.push()で遷移してDOMが消し飛んでも、 defaultValueに持ってる値を突っ込んであげれば、取り敢えず入力したデータを保持することが出来ます
反対にSPAの外、別のサイトに遷移してからブラウザバックしたときは、メモリの中身が飛んでるので入力したデータが保持されません

各手法の実装方式

ちゃちゃっと書いたのでコードは超雑です

確認環境

Env Ver
react 17.0.2
react-dom 17.0.2
react-router-dom 5.2.0

1. フォームの入力値をDOMに保持させるステートレスな方式

大正義ステートレスです
Function Componentはステートレスなので、こうあってほしいですよね~
コードもシンプルで管理しやすいのが素敵なところです

export const StatelessPage = () => {
  const history = useHistory();
  return (
    <div>
      <form>
        <input type="text" />
        <button onClick={() => history.push(AppRoute.dom2.path)}>submit</button>
      </form>
    </div>
  );
};

2. フォームの入力値をJSに保持させるステートフルな方式

ギルティなステートフル方式です
Class Componentを捨てたはずなのにどうしてこうなった…
コードが煩雑で管理が大変です

export const StatefullPage = () => {
  const ctx = useContext(ExampleContext);
  const history = useHistory();
  const onChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
    ctx.text = ev.target.value;
  };

  return (
    <div>
      <form>
        <input
          type="text"
          defaultValue={ctx.text}
          onChange={(ev) => onChange(ev)}
        />
        <button onClick={() => history.push(AppRoute.ctx2.path)}>submit</button>
      </form>
    </div>
  );
};