[TS] 개발자처럼 사용하기 5 : 타입 좁히기

Zero·2024년 2월 11일
0
post-thumbnail

살펴보게된 계기

이번에 멘토링을 진행하게 되면서 많은 질문을 받았던 것중 하나가 as 와 같은 타입 단언을 하지 않고 어떻게 타입을 정해주는 것인지 였다.

as를 사용하는 것이 나쁜 것인가? 라고 묻는다면 이것은 확답할 수 없습니다.

export async function handleSearchingUser(formData: FormData) {
  const name = formData.get('user_name') as string;
  const kakaoId = formData.get('user_kakao') as string;

  try {
    const user = await prisma.applicant.findFirstOrThrow({
      where: {
        apply_name: name,
        apply_kakao_id: kakaoId,
      },
    });
  } catch (err) {
    // ...
  }
}

위 코드에서 formData.get('user_name')을 하게 될 경우 반환되는 데이터의 타입은 다음과 같습니다. FormDataEntryValue | null

하지만 name이나 kakaoId를 사용하는 로직을 보면 string 타입을 전달해야 하는 것을 알 수 있습니다.

타입 단언을 안쓰고도 구현을 할 수 있습니다.

export async function handleSearchingUser(formData: FormData) {
  const name = formData.get('user_name');
  const kakaoId = formData.get('user_kakao');

  if (typeof name !== 'string' || typeof kakaoId !== 'string') {
    throw new Error('Invalid input type');
  }

  try {
    const user = await prisma.applicant.findFirstOrThrow({
      where: {
        apply_name: name,
        apply_kakao_id: kakaoId,
      },
    });
    console.log(user.apply_intro);
  } catch (err) {
 	// ...
  }
}

이처럼 타입을 좁혀주면서 다음에 해당 변수를 사용할 경우 해당 변수의 타입을 걸러줄 수 있습니다.

ex) 만약 타입이 해당 타입이 아닐 경우 이곳을 통과하지 못한다.

타입스크립트 문제를 풀어보면 다음과 같은 문제를 받았던 적이 있습니다.

export enum TodoEnum {
  DAILY = 'DAILY',
  WEEKLY = 'WEEKLY',
  MONTHLY = 'MONTHLY',
}

export type TodoDataBase =
  | {
      type: TodoEnum.DAILY;
      content: string;
      title: string;
    }
  | {
      type: TodoEnum.WEEKLY;
      total: Date;
    }
  | {
      type: TodoEnum.MONTHLY;
      goal: string;
    };

export type TodoType<T extends TodoEnum = TodoEnum> = Extract<TodoDataBase, { type: T }>;

TodoType은 TodoEnum이라는 열거형 타입을 매개변수로 받고, 해당 매개변수에 해당하는 필드를 가지고 있는 타입을 추출하는 역할을 합니다.

만약 아무것도 전달을 받지 않았다면 모든 필드에 접근을 할 수 있는 것입니다.

이를 활용하여 여러 타입을 받는 One Component를 구성을 해보았습니다.

const OneTodo: React.FC<{ todo: TodoDataBase }> = ({ todo }) => {
    const Todo = useMemo(()=> {
      
      if (isMonthly(todo)) {
        const header = todo.type;
        const content = todo.goal;
        
        return {header, content}
      } else if (isWeekly(todo)) {
        const header = todo.type;
        const content = todo.total.toISOString();
        
        return {header, content}
      } else {
        const header = todo.type;
        const content = todo.content;  
        
        return {header, content}
      }
  }, [todo.type]) 

    return (
    <div>
        <div>
          {Todo.header}
        </div>
        <div>
        {Todo.content}
        </div>
      </div>
    )
};
export default OneTodo;
// /util/typeGuard

import { ITodoDaily, ITodoMonthly, ITodoWeekly } from '@apis/1';
import { TodoDataBase, TodoEnum } from '@type/todo';

export const isDaily = (todo: TodoDataBase): todo is ITodoDaily => {
  return todo.type === TodoEnum.DAILY;
};

export const isWeekly = (todo: TodoDataBase): todo is ITodoWeekly => {
  return todo.type === TodoEnum.WEEKLY;
};

export const isMonthly = (todo: TodoDataBase): todo is ITodoMonthly => {
  return todo.type === TodoEnum.MONTHLY;
};

해당 코드는 typeGuard문을 통하여 타입을 좁혀주고 있습니다.

만약 isWeekly안에 들어왔다가 나간 값이 true일 경우 받아온 todo의 타입은 ITodoWeekly가 됩니다.

useMemo를 통해 변수를 만들어줌으로서 다른 타입의 값이 들어올 경우에도 OneTodo하나를 사용하여 각 타입에 맞는 값을 컴포넌트를 보여주도록 할 수 있습니다.

profile
0에서 시작해, 나만의 길을 만들어가는 개발자.

0개의 댓글