이번에 멘토링을 진행하게 되면서 많은 질문을 받았던 것중 하나가 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
하나를 사용하여 각 타입에 맞는 값을 컴포넌트를 보여주도록 할 수 있습니다.