치악산 복숭아

내가 보려고 정리한 - TypeScript로 실제 세상에 존재하는 타입을 만들기 - 를 보고 본문

etc

내가 보려고 정리한 - TypeScript로 실제 세상에 존재하는 타입을 만들기 - 를 보고

Juliie 2025. 1. 26. 23:07

원본 영상

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