かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

今更の TypeScript enum 型になっているプロパティの値ってどう指定するの?

GraphQL の schema から生成した型を利用してモックデータを作っていました。
今回 GraphQL の schema に特定の文字列が返されるフィールドを作成したく列挙型 (Enum) で Schema を定義していました。

schema.graphql

emun IdolType {
  cute
  cool
  sexy
  pop
}

type Idol {
  id: ID!
  name: String!
  type: IdolType!
}

type Query {
  idols: [Idol]
}

👇 生成された TypeScript の型
graphql.ts

export enum IdolType {
  Cute = 'cute',
  Cool = 'cool',
  Sexy = 'sexy',
  Pop = 'pop'
}

export type Idol = {
  __typename?: 'Idol';
  id:  Scalars['ID'];
  name:  Scalars['String'];
  type: IdolType;
};

このとき Idol 型のデータを作成しようとした時に type の値の定義方法で TypeError になってハマりました

enum 型 の値は enum の定義そのものを使う必要がある

🙅 エラーになったパターン

enum 型に含まれている値を直接指定するとエラーになりました

import { Idol } from './graphql';

const Hosimiya: Idol = {
  id: '1',
  name: '星宮いちご',
  type: 'cute',
}

=> Types of property 'type' are incompatible. Type '"cute"' is not assignable to type 'IdolType'

🙆 enum 型の型定義を使って指定する

- import { Idol } from './graphql';
+ import { Idol, IdolType } from './graphql';

const Hosimiya: Idol = {
  id: '1',
  name: '星宮いちご',
- type: 'cute',
+ type: IdolType.Cute,
}

enum 型は使わないほうが良い。みたいな記事を結構見たことがあったのと Union 型とかで何とかなってたから今まで触れてきませんでした。今回 GraphQL からの型生成で enum 型に初めましてした結果 Union 型みたいに直接値を指定するとエラーになって何でや!ってなったのでした…

雰囲気エンジニアリングしかできないから力不足を感じと同時に TIL (Today I Learned) を感じる日々です。がんばるぞぃ ₍ ᐢ. ̫ .ᐢ ₎


[参考]

Enum ずっとエミュって読んでた… エミューは Emu だった… えいごちから…

React (TypeScript) 画像をプレビューするコンポーネント作ってみた

Twitter みたいに画像を投稿できるフォームでは input[type="file"] を使いますがデフォルトでは選択した画像のファイル名が表示されるだけなので、何の画像を選択したかプレビューができたほうが便利です。
割とフォーム作成の際に必要になるので思いつきで React のコンポーネント化してみましたのメモ。

JavaScript FileAPI で input[type="file"] で選択されている画像のプレビューを表示する方法

HTML

<input type="file" id="inputFile">

JavaScript

const inputfile = document.getElementById('inputFile')
const image = document.createElement('img')

inputfile.addEventListener("change", function(evt) {
  if (!evt.target.files || !evt.target.files[0]) {
    image.remove();
    return;
  }

  const file = evt.target.files[0];
  const reader = new FileReader();
  // reader.readAsDataURL でファイルの読み込みが完了したら <img> タグにして出力する
  reader.onload = () => {
    image.src = reader.result;
    inputfile.insertAdjacentElement('afterend', image);
  }
  reader.readAsDataURL(file);
}, false);
サンプル

See the Pen File Preview by KIKIKI (@kikiki_kiki) on CodePen.

👇 React Component 化する

File API を使ってプレビュー画像を表示する React Component

// ImagePreview.tsx
import { FC, useEffect, useState } from 'react';

type ImagePreviewProps = {
  file: File | null
};

export const ImagePreview: FC<ImagePreviewProps> = ({ file, ...props }) => {
  const [url, setUrl] = useState<string>('');
  const isLoading = file && !url;

  useEffect(() => {
    if (!file) {
      return;
    }

    let reader: FileReader | null = new FileReader();
    reader.onloadend = () => {
      const res = reader!.result;
      if (res && typeof res === 'string') {
        setUrl(res);
      }
    };
    reader.readAsDataURL(file);

    return () => {
      reader = null;
    };
  }, [file]);

  return file ? (
    isLoading ? (
      <Skelton  /> {/* Loader */}
    ) : (
      <img src={url} alt={file.name} {...props} />
    )
  ) : null;
};

App.tsx

import { FC, useCallback, useState } from 'react';
import { ImagePreview } from './ImagePreview';

const App: FC = () => {
  const [file, setFile] = useState<File | null>(null)

  const changeFileHandler = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => {
    if (evt.currentTarget?.files && evt.currentTarget.files[0]) {
      setFile(evt.currentTarget.files[0]);
    }
  }, []);
  
  const resetHandler = useCallback(() => {
    setFile(null);
  }, [])
  
  return (
    <div>
      <input type="file" onChange={changeFileHandler} />
      {file && (
        <div>
          <button type="button" aria-label="Close" onClick={resetHandler} />
          <ImagePreview file={file} />
        </div>
       )} 
    </div>
  );
};
サンプル

See the Pen File Preview with React by KIKIKI (@kikiki_kiki) on CodePen.

所感

FileAPI を使ってプレビュー表示機能はなんか毎回都度作ってて、その度にどうやるんだっけ?って忘れる…
React Component はぱっと思いつきで作ったものなので、もっといい方法がありそうな気がする。Component を返す Hooks にして isLoading とかを別に取れるようにしたほうが便利かな〜
まぁ動いてるから良し ₍ ᐢ. ̫ .ᐢ ₎


[参考]

ずっと平家物語みてた。良い…

TypeScript 特定のキーを持つオブジェクトの型

オブジェクトの特定のプロパティの値をアップデートをする関数を作成していました

interface Todo {
  name: string;
  description: string;
}

const todo: Todo = { name: "", description: "" };

const update = (arg: {name?: string; description?: string}): Todo => {
  return {
    ...todo,
    ...arg
  };
};

update 関数の引数の型を interface を使っていい感じにしたい…

NG: [key: union 型] ではエラーになる

{ [key: 'name' | 'description']?: string } という型を作ればいいと思ったのですが、これはエラーになってしまいます

type ArgType = { [key: 'name' | 'description']?: string };

=> An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead

OK: [key in union 型] を使えばOK

[key in 'name' | 'description'] という形であればOK

type ArgType = { [key in 'name' | 'description']?: string };
/* 👇
type ArgType = {
    name?: string | undefined;
    description?: string | undefined;
}
/*

interface から union型を作る

interface Todo の key の集合は keyof Todo で取得できるので、[key in keyof Todo] とすれば良い
Mapped types って言うらしい

interface Todo {
  name: string;
  description: string;
}

const todo: Todo = { name: "", description: "" };

const update = (arg: [key in keyof Todo]?: string}): Todo => {
  return {
    ...todo,
    ...arg
  };
};

₍ ᐢ. ̫ .ᐢ ₎ できた。

やったことある気がする…って思ってたら案の定同じような記事を過去に書いてたぬ…


[参考]

カラマリ・ユニオン良いよね…