TypeScript[React]

JavaScript

ReactでTypeScriptを書いていくポイントのまとめ

参考動画↓

まずjsx関数が返すエレメントはJSX.Elementです。これはデフォルトで設定されているのですが、明示することもできます。別の値やnullなどを返すことがないよう明示する方がよいかもしれません。

function Component():JSX.Element {
  return (
    <div>
      <h2>React & Typescript</h2>
    </div>
  );
}
export default Component;

props

propsに値を渡すとき、型が指定されてないのでエラーが出ます。渡される側のコンポーネントで引数に型付をすることができます。

function Component({name, id}:{name:string; id:number}) {
  return (
    <div>
      <h2>React & Typescript</h2>
    </div>
  );
}
export default Component;

注:型指定しても赤線が出ますが、使われてない引数というエラーです。

タイプ型を作成することもできます

type ComponentProps = {
  name:string; 
  id:number
}

function Component({name, id}:ComponentProps) {
  return (
    <div>
      <h1>Name:{name}</h1>
      <h1>id:{id}</h1>
    </div>
  );
}
export default Component;

propsからの記述も可能

type ComponentProps = {
  name:string; 
  id:number
}

function Component(props:ComponentProps) {
  return (
    <div>
      <h1>Name:{props.name}</h1>
      <h1>id:{props.id}</h1>
    </div>
  );
}
export default Component;

propsに関数を渡すとき

propsに関数を渡すとき、関数の型をタイプで定義することができます。

type addTaskProp = {
  addTask:(task:Task)=> void;
}
function Form({addTask}:addTaskProp) {
    const [text, setText] = useState("");
    function hundleSubmit(e:React.FormEvent<HTMLFormElement>){
      e.preventDefault();
      addTask({
        id:new Date().getHours().toString(),
        description:text,
        isCompleted:false,
      });
    }
  return (
    <form onSubmit={hundleSubmit}>
        <input type="text" value={text} onChange={(e)=>setText(e.target.value)}/>
        <button type="submit">add task</button>
    </form>
  )
}

子コンポーネントの型付

タグで子コンポーネントをはさむと、子コンポーネントの型がわからないのでエラーがでます。

function App() {
  return (
    <main>
      <Component name='peter' id={123}>//エラーが出る
        <h2>hello world</h2>
      </Component>
    </main>
  );
}

コンポーネントの引数にchildrenをいれ、React.ReactNodeと型付けることにより解決します。

type ComponentProps = {
  name:string; 
  id:number;
  children:React.ReactNode;
}

function Component({name, id, children}:ComponentProps) {
  return (
    <div>
      <h1>Name:{name}</h1>
      <h1>id:{id}</h1>
      {children}
    </div>
  );
}
export default Component;

PropsWithChildrenをインポートすれば以下のようにも書ける。React.ReactNodeと型付を書く必要がなくなるので便利。

import { type PropsWithChildren } from "react";

type ComponentProps = PropsWithChildren<{
  name:string; 
  id:number;
}>;

function Component({name, id, children}:ComponentProps) {
  return (
    <div>
      <h1>Name:{name}</h1>
      <h1>id:{id}</h1>
      {children}
    </div>
  );
}
export default Component;

stateの型付

useStateなどの型付はジェネリック型であらかじめ型付されているため、あとは型指定するだけです。numberやstringなどは初期値を入れることで自動的に型推論が行われるので型指定する必要はないですが、arrayなどで初期値を空の配列[]にすると、型がnever[]になってしまいます。そういう場合はジェネリック型の型指定で明示的に定義しましょう。

 const [text, setText] = useState('hello');//型はstringになっている
 const [list, setList] = useState<string[]>([]);//型を指定する必要がある

タイプを作って指定することもできます。

type Link = {
  id:number;
  url:string;
}

const navLinks = [
  {
    id:1,
    url:'some url',
  },
  {
    id:2,
    url:'some url',
  },
]

function Component() {
  const [links, setLinks] = useState<Link[]>(navLinks);

元のオブジェクトにタイプ型を指定するとuseStateに指定する必要もなくなります。

type Link = {
  id:number;
  url:string;
}

const navLinks: Link[] = [
  {
    id:1,
    url:'some url',
  },
  {
    id:2,
    url:'some url',
  },
]

function Component() {
  const [links, setLinks] = useState(navLinks);

event

フォームなどのonChange, onSubmitイベント時の引数への型付です。

function Component() {

  const [email, setEmail] = useState('');
  const handleChange = (e:React.ChangeEvent<HTMLInputElement>) => {
    setEmail(e.target.value);
  }
  return (
    <div>
      <form className="form">
        <input 
        type='email' 
        value={email} 
        onChange={handleChange}
        />
        <button type="submit" className="btn btn-block">submit</button>
      </form>
    </div>
  );
}

渡されるイベントはReact.ChangeEventという型がついており、これもジェネリック型で型指定するようになっています。上記の場合、input要素を渡しているので<HTMLInputElement>を指定しています。タグ内にインラインでそのまま記述するときはTypeScriptが型を把握しているので型付は必要ありません。

ちなみにFormにsubmitイベントをつけるときの引数の型は以下のようになります。

  const handleSubmit = (e:React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
  }

new FomDataで値を取得するときは以下のように書くことで型が指定されます。

  const handleSubmit = (e:React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.target as HTMLFormElement);//asを使う
    const formData = new FormData(e.currentTarget);//currentをつける
    const data = Object.fromEntries(formData);
    console.log(data);
  }

このフォームデータの値を使うときはnullかもしれないためエラーが出ます。それを防ぐためにasで型を上書きして指定します。

   const text = formData.get('text') as string;
    const person:Person = {
      name:text
    }

//または

   const data = Object.fromEntries(formData);
    const person:Person = { name:data.text as string }

Context

contextを作成する場合、最初に何が入るかわからないのでundefinedにすると他の型も入らなくなるため、ジェネリック型で複数の型を指定しておくことができます。

import { createContext } from 'react';

const ThemeProviderContext = createContext<string | undefined>(undefined);

export function ThemeProvider({children}:{children:React.ReactNode}){
    return <ThemeProviderContext.Provider value='hello'>
        {children}
    </ThemeProviderContext.Provider>
}

オブジェクトを渡すときは以下のように型付します。

const ThemeProviderContext = createContext<{name:string} | undefined>(undefined);

export function ThemeProvider({children}:{children:React.ReactNode}){
    return (
        <ThemeProviderContext.Provider value={{name:'hello'}}>
            {children}
        </ThemeProviderContext.Provider>
    );
}

デフォルトの値を設定するとき

type Theme = 'Light' | 'dark' | 'system';

type ThemeProviderState = {
    theme:Theme,
    setTheme:(theme:Theme) => void;
};

type ThemeProviderProps = {
    children:React.ReactNode;
    defaultTheme?:Theme;
}

export function ThemeProvider({children, defaultTheme = 'system'}:ThemeProviderProps){
    const [theme, setTheme] = useState<Theme>(defaultTheme);
    return (
        <ThemeProviderContext.Provider value={{theme, setTheme}}>
            {children}
        </ThemeProviderContext.Provider>
    );
}

useReducer

stateやactionにもタイプ型を作って型指定することができます。

export type CounterState = {
    count:number;
    status:string;
}

export const initialState:CounterState = {
    count:0,
    status:'Pending',
}

type UpdateCountAction = {
    type:'increment' | 'decrement' | 'reset',
}

type SetStatusAction = {
    type:'setStatus';
    payload:'active' | 'inactive';
}

type CountAction = UpdateCountAction | SetStatusAction;

export const counterReducer = (state:CounterState, action:CountAction):CounterState =>{
    switch(action.type){
        case 'increment':
            return  {...state, count:state.count + 1};
        case 'decrement':
            return {...state, count:state.count - 1};
        case 'reset':
            return {...state, count:0};
        case 'setStatus':
            return {...state, status:action.payload};
        default:
            return state
    }

//使うとき
<button onClick={()=>dispatch({type:'setStatus', payload:'active'})} className="btn">
     Set Status to Active
</button>

選択肢がもう無いということを明示するためneverを使うこともできます。
わかりやすくエラーを投げることもできます。

export const counterReducer = (state:CounterState, action:CountAction):CounterState =>{
    switch(action.type){
        case 'increment':
            return  {...state, count:state.count + 1};
        case 'decrement':
            return {...state, count:state.count - 1};
        case 'reset':
            return {...state, count:0};
        case 'setStatus':
            return {...state, status:action.payload};
        default:
            const unhandledActionType:never = action
            throw new Error(
                `Unexpected action type: ${unhandledActionType}.Please double check the counter reducer.`
            )
    }
}

Fetch API

zodなどのライブラリを使って返ってくるデータを検証するなど、返ってくるデータの型を定義して型指定すると安全です。

 const [comments, setComments] = useState<Comment[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState<string | null>(null);

  useEffect(()=>{
    const fetchData = async()=>{
      setIsLoading(true);
      try{
        const response = await fetch(url);
        if(!response.ok){
          throw new Error(`Failed to fetch data...`)
        }
        const rawDatabefore = await response.json();
        const rawData:Comment[] = rawDatabefore.comments;
        const result = commentSchema.array().safeParse(rawData);

        if(!result.success){
          console.log(result.error.message);
          throw new Error(`Failed to parse comment`)
        }
        setComments(result.data);
      }catch(err){
        const message = err instanceof Error ? err.message : 'there was an error...';
        setIsError(message);
      }finally{
        setIsLoading(false);
      }
    };
    fetchData();
  }, []);

if(isLoading) {
  return <h3>Loading...</h3>;
}
if(isError) {
  return <h3>Error {isError}</h3>;
}

  return (
    <div>
      <h2 className='mb-1'>Comments</h2>
      {comments.map((comment)=>{
        return <p key={comment.id} className='mb-1'>{comment.body}</p>
      })}
      
    </div>
  );

上記は別ページでCommentという返ってくるデータのタイプ型を作成し指定し、インポートして使っています。