なぽろぐ

気ままに感じたことを記事にまとめます。Vtuberのイベントのことと、プログラム関連のことが多めだと思います。

インターネットとリアルが混じって恥ずかしくなる瞬間

ハンドルネームは名前っぽいほうがええ!

僕のハンドルネールはNaporitanとかなぽりたんでやっています。

会社では「なぽりくん」って呼ばれています。インターネットでは「なぽ」ってよばれてることがおおいです。

社内で初めて合う人とかに軽く自己紹介的なことするじゃないですか、

〇〇です。こんちには。
今日はこれこれするためにやってきました

みたいなこと言うじゃないですか......

その時毎回、「本名じゃないんですが」とか「社内ではこう呼ばれてて」っていうprefixをつける必要があるんですよね。

なんかこう・・・・・ちょっと恥ずかしい!!!!

それもこれも、「なぽりたん」とかいう明らかに食べ物名をハンドルネームにしてるからですね・・・・

ハンドルネームは名前っぽい名前にしてないと初対面に自己紹介するときにちょっと恥ずかしいよって話でした。

flowからTypeScriptへ移行するときこんな感じでやればいいんじゃないかという案

flowからTypeScriptへ

  1. ts-migrateをつかう
  2. 新しく作るページをts化して参照するモジュールにd.tsをかく

ts-migrateを使う

これで倒せるなら最高だが、テストを書いてないと主要なページすべてを動作確認する必要がありそう。ASTで機械的に変更すると言っても何らかのバグを踏み抜いてでかいバグをプロダクションで起こすのはまずい。

e2eテスト、単体テスト、QAを毎回やってるチームなら行けそう。時間が捻出できればだが......

github.com

新しいページをts化参照するモジュールにd.tsをかく

とりあえず始められそうでいいんじゃないかなと思った手法。ts化が完了したらd.tsが書いてあるファイルを触った人がjs, d.tsをけしてtsファイルになおしていく感じ。

ただd.tsを書いた後に、古いページを改修することになってd.tsを書いたjsをいじることになったら二度手間感ある....。古いページに手を入れるときにそのページもts化してね〜ってやったときのコストと相談だよな〜て感じもする。

最後に

2番目のほうが受け入れられやすい気がする。どうですか?ASTでバコって変換する系はts-migrate最強感ありますよね〜

flowに対する気持ち

flowのextensionをVSCodeに入れるととんでもなく重くなるんですよね。git switchするとVSCode固まる......。(解決策知っている人いたら教えて下さい)

TypeScriptはhttps://github.com/microsoft/TypeScriptでコード読んでる分には全然重くなかったし、ホバーして推論結果出すのも爆速だった気がする。

flowはanyに対して許容的というかimport type { Hoge } from "huga"ってやったときにhugaの型定義がinstallされて無くてもHogeがanyとして扱われるだけで何も怒らないので、そこはやだな〜って思います。

ひたすら文献が少ない。これに尽きる....utility typeとplaygroundとにらめっこしてflowのこれはTypeScriptで書くんだよ〜みたいな謎技術を得てしまった。

もしflowからTypeScriptに変換する際に型定義で困ったときは声かけてください。めちゃくちゃ複雑じゃない限り解けると思います。

input rangeのcssをカスタマイズしたときに進んだ量の色が消える問題

<input type="range" />css`-webkit-slider-runnable-trackで困った話

こいつの話です

こいつにcssを当てる際-webkit-slider-runnable-trackを使うと思うんですが(firefoxもありますよねmozなんとか)、backgroundの色変えたくなるじゃないですか・・・・・・・

こんなcss書くと

<style>
input[range]::-webkit-slider-runnable-track {
 background: red;
</style>
<input type="range" />

こうなるんす........

f:id:Naporitan:20201005012440p:plain

お前の青い部分の色だけ変わってくれればよかったんだけどな........ってなったんですが

webkitのprogressだけの色を変えるcssは非標準らしいんですね...... developer.mozilla.org

なので擬似的に再現する必要がありますと....僕はReactで次のように考えました

linear-gradient使ったら行けるんじゃねって

import React, { useState } from "react";
import styled from "styled-components";

export default function App() {
  const [value, setValue] = useState(0);
  return <Range value={value} onChange={(e) => setValue(parseInt(e.target.value, 10))} />;
}

const Range = styled.input.attrs({ type: "range", min: 0, max: 100 })<{ value: number }>`
  &::-webkit-slider-runnable-track {
    background: red;
    background: linear-gradient(to right, red ${(props) => props.value}%, white ${(props) => props.value}% 100%);
  }
`;

ぽいのができました。もっといいやり方あったら教えて下さい:pray:

Image from Gyazo

normalizrの型定義を書いた

Normalizrの型定義を(制約付きで)書いた

normalizrのhelper型です。こうなってくれているといいなぁと思って書きました。

読むのめんどくさいよ〜って人はサンプルを用意したのでcodesandboxか手元の環境でガチャガチャいじってみてください https://github.com/naporin0624/normalizr_ts

Normalizrというライブラリについて

normalizrというライブラリがあります。これはobjectをユーザが定義した構造(normalizrはこれをschemaとよんでいる)に従って分解できるライブラリでRedux公式ドキュメントに使うといいよって書かれてたりします。Normalizing State Shape | Redux

何ができるか

GitHubに大まかな挙動・APIドキュメントが存在しているので食わせるデータと結果だけを書きます。

食わせるデータ

{
  "id": "123",
  "author": {
    "id": "1",
    "name": "Paul"
  },
  "title": "My awesome blog post",
  "comments": [
    {
      "id": "324",
      "commenter": {
        "id": "2",
        "name": "Nicole"
      }
    }
  ]
}

ユーザがschemaを定義します。

import { normalize, schema } from 'normalizr';

// Define a users schema
const user = new schema.Entity('users');

// Define your comments schema
const comment = new schema.Entity('comments', {
  commenter: user
});

// Define your article
const article = new schema.Entity('articles', {
  author: user,
  comments: [comment]
});

const normalizedData = normalize(originalData, article);

normalizedDataの結果がこうなります。

{
  result: "123",
  entities: {
    "articles": {
      "123": {
        id: "123",
        author: "1",
        title: "My awesome blog post",
        comments: [ "324" ]
      }
    },
    "users": {
      "1": { "id": "1", "name": "Paul" },
      "2": { "id": "2", "name": "Nicole" }
    },
    "comments": {
      "324": { id: "324", "commenter": "2" }
    }
  }
}

やってくるjsonをユーザが定義したschemaごとに分解してentities配下に置かれます。resultnormalizrにあるdenormalizeに食わせることでもとのjsonを得ることができるというものです。ちなみにdenormalizeは次のように記述します。

import { denormalize } from 'normalizr';
const denormalizedData = denormalize(result, article, entities /* jsonのentitiesを入れる */ );

かなり柔軟にschemaは意義できます。normalizr/api.md at master · paularmstrong/normalizr · GitHub

TypeScriptの型定義を見てみる

normalizr/index.d.ts at master · paularmstrong/normalizr · GitHub

export function normalize<T = any, E = { [key:string]: { [key:string]: T } | undefined}, R = any>(
  data: any,
  schema: Schema<T>
): NormalizedSchema<E, R>;

export function denormalize(
  input: any,
  schema: Schema,
  entities: any
): any;

かなりanyだらけになっていてnormalizeするとき投入したdataに対してどのschemaがマッチするのかわからないです。またdenormalizeも同じで投入するinputに対してどのschemaを入れればよいのかわからない状態になっています。かなり柔軟にschemaを定義できるようにした代償なのかなと思っている。(僕も柔軟なschemaを維持したまま型定義を書こうとして辛くなってやめた過去もあります。)なので今回はschemaの作り方に制約をもたせて型定義を書いていきます。

目指すところ

  • entity型に含まれるentityを探し出してschema.Entityの第2引数に必要なschema.Entityの組が推論される
  • normalizeの第1引数に入れた型からほしいschema.Entityを推論する
  • denormalizeのReturnTypeを推論する

normalizrのschemaに制約をもたせる

A, B, Cはそれぞれの対応関係を型定義で表したものです。これからこの型定義のことをentityといいます。 Cの型を持つjsonが飛んできてnormalizeに食わせるとA, B, Cをentities配下に持ち, Cのidをresultに持つオブジェクトが返ってくるようなイメージです。

型定義を作るにあたってentityに制約をもたせます。 1. entityは必ずidを持つ 2. entityの持つプロパティにはentity同士のunion型を許容しない - C型のfのような形は許可しません - c, d, eのような形は問題ないです 3. entityの持つプロパティにはentityを含むPropertySignatureを許容しない - C型のgのような形は許可しません

type A = {
  id: string | number; // required
  hoge: string;
  huga: number;
}
type B = {
  id: string | number; // required
  cat: string[];
  dog: string[];
}
type C = {
  id: string | number; // required 
  a: number; // ok
  b: number | string; // ok
  c: A; // ok
  d: A[]; // ok
  e: A | null; // ok
  f: A | B; // ng
  g: { a: A } // ng
}

schemaに制約をもたせてnormalizrの型を書いてみる

実装するにあたりこの型はentityであるということを示すためにBaseEntityという型をつくりentityを作るときはこれをマージすることにしました。

type IUser = {
  name: string;
  address: string;
  phoneNumber: number;
} & BaseEntity;

構造を定義するschemaの型定義をかく

RelatedEntitiesでGenericで渡されてきたentityの中で使われているentityを探し出します。探し出されたrelatedEntitiesがそのままnew schema.Entityの第2引数に渡るのでentityの型からnormalizrに渡すschemaを決定することができます。

import { schema } from "normalizr";

export const createEntity = <T extends BaseEntity>(
  entityName: string,
  relatedEntities: RelatedEntities<T>,
): schema.Entity<T> => new schema.Entity<T>(entityName, relatedEntities);

RelatedEntities型

RelatedEntities型はGenericを受け入れます。Find型により、受け入れたTを1層だけmapped typeで走破してBaseEntityが持つkeyを両方持っているものをentityとみなして新たに型を作り上げていきます。BaseEntityは型情報としては持っているが値としては入れられることもないし、認識することもない_$entityというプロパティを持っています。これのおかげでidだけをプロパティとしてもっていてもentityとして検知されないようにしています。つまり、BaseEntityをマージしたものだけをentityとしてすく上げることができるようになります。

すくい上げられたentityだけを含む型に対してnormalizrschema.Entity型を付与していきます。

import { schema } from "normalizr";

type BaseEntity = { id: string; _$entity?: unknown; };

type NestArray<T> = T | Array<NestArray<T>>;
type Flatten<T extends NestArray<unknown>> = T extends NestArray<infer I> ? I : never;
type BaseType<T> = Exclude<Flatten<T>, null | undefined>;
type FindKeys<T> = { [K in keyof T]: keyof BaseEntity extends keyof BaseType<T[K]> ? K : never }[keyof T];
type Schema<T> = T extends Array<infer U> ? [Schema<U>] : schema.Entity<T>;
type DictSchema<T extends { [key: string]: unknown }> = { [K in keyof T]: Schema<Exclude<T[K], undefined | null>> };

type Find<T> = { [K in FindKeys<T>]: T[K] };

type RelatedEntities<T extends { [key: string]: unknown }> = DictSchema<Find<T>>;

normalizeの型を書く

normalizeの型は割と簡単です。受け入れるTに対応したschemaを待ち受ければよいだけです。 返り値はresultとentitiesがありますが、entitiesは今回かんたん化のためにユーザが定義したentityをimportしてEntitiesという型を作っています。(ここも推論できるのがより良いですが、実現できなかったのでこのようにしました:sob:)

EntitiesのなかにあるNormalizedEntity型はGenericで渡されるentityの中からentityを探し出してstringまたは, stringの配列にしてくれるものです。entityを含むentityをnormalizeするとentityがあった部分には他のentityへの参照であるidが格納されているからです。(normalizedDataを見るとわかると思います)

import { schema, normalize, NormalizedSchema } from "normalizr";

type Schema<T> = T extends Array<infer U> ? [Schema<U>] : schema.Entity<T>;
type ArrayToString<T> = T extends Array<infer U> ? Array<ArrayToString<U>> : string;
type NormalizedEntity<T extends { [key: string]: unknown }> = {
  [K in keyof T]: K extends keyof Find<T> ? ArrayToString<T[K]> : T[K];
};
type Entities = Partial<{
  // 定義したentity型をimportしてnormalizeされたときに排出されるentitiesの型とする
  users: { [key: string]: NormalizedEntity<IUser> };
  groups: { [key: string]: NormalizedEntity<IGroup> };
  addresses: { [key: string]: NormalizedEntity<IAddress> };
}>
type Result<T> = T extends Array<infer U> ? Array<Result<U>> : string;

export const normalizer = <T>(data: T, schema: Schema<T>): NormalizedSchema<Entities, Result<T>> =>
  normalize<T, Entities, Result<T>>(data, schema);

denormalizeの型を書く

denoramlizedata(単一のidもしくはidの配列)とschema(dataが分解される前の構造情報)とentitiesを渡せばよいです。

dataが単一ならschemaも単一で, dataが配列ならschemaも配列で与えるように型を書いていきます。dataからはdenormalizeのReturnTypeを推論することはできないので, 渡されたschemaからentityの型をGenericを用いて取り出します。Genericで取り出されたentityの型Sを渡されたdataの型に合わせて整形し直します。 denoamlizeしたときに第3引数のentitiesに必要なデータがないときdataが単一ならundefined, 配列なら[]が返されるのでDenormalized型を使ってそれを表現します。

import { schema, denormalize } from "normalizr";

type NestArray<T> = T | Array<NestArray<T>>;
type Entity<D, T> = D extends Array<infer U> ? [Entity<U, T>] : schema.Entity<T>;
type Denormalized<D, S> = D extends Array<infer U> ? Array<Denormalized<U, S>> : S | undefined;

export const denormalizer = <D extends NestArray<string>, S, T extends Entities>(
  data: D,
  schema: Entity<D, S>,
  entities: T,
): Denormalized<D, S> => denormalize(data, schema, entities);

使ってみる

こちらにサンプルコードをpushしたのでcheckoutすればすぐ使えます。GitHub - naporin0624/normalizr_ts codesandboxにも上げましたempty-glade-xxys6 - CodeSandbox

createEntity

src/entities/group.tsをみてください

import { IUser, userEntity } from "./user";
import { createEntity } from "../normalizer";
import { BaseEntity } from "../types";

export type IGroup = {
  users: IUser[];
} & BaseEntity;

export const groupEntity = createEntity<IGroup>("groups", { users: [userEntity] });

IUserがentityなのですがcreateEntityの第2引数からusersを消すと型エラーになります。 Image from Gyazo

配列になってないときも型エラーを吐いてくれます。 Image from Gyazo

またsrc/entities/user.tsはIAddressだけがentityでありcomplex, keysが持つ型にはcreateEntityが反応してないことがわかります。 BaseEntityをマージしてないとentityとして認識されないためこの型がentityなのかそうでないのかがしっかり分類できています。 f:id:Naporitan:20201005010616p:plain

normalizer

与えられたgroupオブジェクトからほしいschemaを導き出せています。resultの型も渡されたgroupオブジェクトは単一なのでstringが返ってきています f:id:Naporitan:20201005010605p:plain

denoramlzer

resultは単一のstring, schemaも単一のgroupEntityなので返り値はIGroup | undefined型になっています。 f:id:Naporitan:20201005010554p:plain

こちらは配列の場合です。同様にうまくいっています。 f:id:Naporitan:20201005010539p:plain

さいごに

僕はReduxを使う際にnormalizrを多用するのでこのようなヘルパー型を作って楽に開発できるようにしています。entityの作り方に制約をもたせていますが、今のところ特に苦しい場面に出会ってません。(複雑な構造に出会ってないだけかも) なので結構良さそうじゃないかな〜と思っています。できるならライブラリにしたいけど型定義だけのライブラリって作る意味あるんだろうか……。normalizrの機能もかなり制限しちゃってるし…

次はこれをどうReduxに使っているのかを紹介できればなと思います。最後まで見てくださった方ありがとうございました!!

もっとこうしたらいいよという提案はいつでも受け付けています!!

codesandboxでvim extensionを有効化して快適に使う方法

codesandboxでvimを有効化したい

File > Preferences > CodesandboxSettings

Enable VIM extensionのトグルボタンをonにすればとりあえずはvscode使用のvim extensionが効く。もしかしたらreloadしないといけないかも

codesandbox editor setting
editor setting

ESC入力するとエディターからのフォーカスが切れちゃうとき

  • Vimiumが悪さしてる可能性が高いです

github.com

このようにcodesandboxだけ除外しておきましょう. codesandbox開いた状態でAdd Rule -> Save Changesすれば効きます

連続入力できない(Mac)

こうなる時がある

こちらの記事を参考にGoogleIMEを入れることで解決した nakamura001.hatenablog.com

ABCになっていると連続入力できないのでGoogleIMEの英数を使用すると治ります

終わりに

codesandbox良すぎる。環境1クリックで作れるし、URL共有すればペアプロもできる。しかも作ったものをすぐSNSに投稿できるのでサクッとなにか作って見せたいときには便利だと思った〜

課金しました

声だけでツイートできるアプリvoitterを作りました

Voitter

ライブ中にキーボードを打ちたくない,でもツイートはしたい!というツイ廃向けかも知れません....

voitter.web.app

使い方

Twitterでログインして

Image from Gyazo

ハッシュタグをつけて

Image from Gyazo

Start押すだけ

Image from Gyazo

あとは喋ればツイートされます!

実際に使ってみると....

こんな感じになります.誤認識とかもあるのですが,それも含めて楽しんでいただけると!

Image from Gyazo

終わりに

MU2020最高すぎてキーボードを打つ時間すらもったいなく感じたので作りました.

ライブとかその辺向けに作ったつもりだったんですけど,誤認識が結構面白かったw

firebase + React + antdでさくっと作れたのでloginだけfirebaseに任せてtokenとるってやり方は結構良さそうだな〜って感じました