치악산 복숭아
내가 보려고 정리한 - TypeScript로 실제 세상에 존재하는 타입을 만들기 - 를 보고 본문
원본 영상
TypeScript로 실제 세상에 존재하는 타입을 만들기 | 2023 INFCON
👷 Type Safe
- 런타임에 타입 에러가 안나는 코드
- 타입 오류가 있다면 빌드 타임(컴파일 타임)에 찾아내는 것
- + 내가 의도하지 않은 타입이 나타나지 않는 것
실제 세상에 있는 타입 ex) 실제 세상에서의 채팅 메시지는 텍스트 타입과 이미지 타입 2가지 종류가 있다.
하지만 아래처럼 ChatMesasge 타입을 정한다면?
export interface ChatMesasge {
id: string;
messageType: string; // "Image" or "PlainText"
imageUrl: string | null;
plainText: string | null;
}
export const chat: ChatMessage = {
id: '1',
messageType: 'abc' // ? 세상에 없는 이상한 타입
imageUrl: null,
plainText:null // ?? 어떤 값도 없는 메세지 ...?
}
Union Type을 사용해 이렇게 고치면 …
export type ChatMesasge = ImageChatMessage | PlainTextChatMessage
interface ImageChatMessage {
id: string;
messageType: 'Image';
imageUrl: string;
}
interface PlainTextChatMessage {
id: string;
messageType: 'PlainText';
plainText: string;
}
export const chat: ChatMessage = {
id: '1',
messageType: *'abc'* // error!
plainText: "안녕하세용"
}
비즈니스 스펙에서 벗어나지 않는 타입을 정의할 수 있게 됐다!
로직도 Type safe하게 짜고싶어
🧩 패턴 매칭
- 쉽게 말하면 조금 똑똑한 switch-case문
- 어렵게 말하면 어떤 Object가 어떤 패턴에 만족하는지 찾는 것
비슷한 기능으로 kotlin에는 when 키워드, Scalar, Rust, Go 등등에도 패턴 매칭이 있지만 타입스크립트에는 없음
하지만 패턴 매칭을 구현한 npm 패키지 ts-pattern 가 있다! (8: 55 ~ )
import { match } from 'ts-pattern';
export type ChatMesasge = ImageChatMessage | PlainTextChatMessage
interface ImageChatMessage {
id: string;
messageType: 'Image';
imageUrl: string;
}
interface PlainTextChatMessage {
id: string;
messageType: 'PlainText';
plainText: string;
}
export const chat: ChatMessage = {
id: '1',
messageType: 'Image',
imageUrl: "https://..."
}
export const sendNotification = (chatMessage: ChatMessage) => {
match(chatMessage)
.with({ messageType: 'Image' }, () => { console.log("messageType이 Image인 경우에는 이 함수를 호출해주세요")})
*.exhaustive() // 왜* ChatMesasge의 *모든 경우를 다 안 적어주는거야 ...? 라는 의미의 에러*
};
sendNotifiaction을 아래 코드처럼 수정하면 경고가 발생하지 않는다
export const sendNotification = (chatMessage: ChatMessage) => {
match(chatMessage)
.with({ messageType: 'Image' }, (message) => { console.log("`message` args의 타입도 자동으로 추론해줘요") })})
.with({ messageType: 'PlainText' }, (message) => { })})
.exhaustive()
};
장점
- 새로운 타입에 대해 대응을 미처 하지 못해도 컴파일 단계에서 잡을 수 있음
참고
Bringing Pattern Matching to TypeScript 🎨 Introducing TS-Pattern
외부에서 온 값에 대한 Validation
- 타입에 대한 fail over, 외부에서 온 값이 내 타입에 맞는지 미리 대비해서 Type safe한 코드를 짜자
예시 코드 (서버)
app.post('/send-message', async(request, reply) => {
const body = request.body as ChatMessage;
// 강제 형변환 -> 타입 체킹 무시 -> type safe하지 않음
await sendMessage(body);
reply.send({
success: true,
});
어떻게 하면 위 코드를 type safe한 코드로 만들 수 있을까?
💎 Zod (validation library)
(이거 외에도 많은 유효성 검증 라이브러리가 있음, 속도가 더 빠르다던가 … 기능이 더 많다던가 …)
import { z } from 'zod';
import { ChatMessage } from '.';
const chatMessageZodeScheme = z.union([
z.object({
id: z.string(), // id에는 string이 들어오는지 검사,
messageType: z.literal('Image'),
imageUrl: z.string(),
}),
z.object({
id: z.string(),
messageType: z.literal('PlainText'),
plainText: z.string()
}),
]);
const app = fastify();
app.post('send-message', async(request, reply) => {
const parseResult = chatMessageZodeScheme.safeParse(검사할 값)
// 파싱에 실패했다면 -> { success: false; error: ZodError }
if (parseResult.success === false) {
reply.status(400);
return
}
reply.send({
success: true,
});
})
...
- 애플리케이션 외부에서 오는 모든 값들은 validation을 걸어주는게 좋은 것 같다.
- type safe하는 코드를 왜 쓰지? → 결국엔 프로그래밍 하는데 실용성을 높여준다고 생각
- 당근 마켓 안에서도 scheme language에 대해, 비즈니스 스펙에 맞고 type safe한 스키마들을 만드는 것에 굉장히 관심이 많고 전사적인 프로젝트도 있다~
참고
Zod로 유효성 검증과 타입 선언의 두 마리 토끼 잡기
TypeScript-first schema validation with static type inference
Comments