TypeScript satisfies

Tech
2024-07-17

배경

// Button.stories.tsx
 
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
 
const meta = {
  title: 'Example/Button',
  component: Button,
} satisfies Meta<typeof Button>; // Type Annotation이 아닌 satisfies 사용
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const Primary: Story = {
  args: {
    primary: true,
  },
};

Storybook을 공부하다가 v7을 기점으로 satisfies 키워드가 등장하는 것을 보고 어떤 점이 달라졌는지, Storybook 말고 또 어디에 활용할 수 있는지 궁금해졌습니다. 이렇게 satisfies를 사용하면 무엇이 달라질까요?

우선 satisfies가 없었던 경우부터 살펴보겠습니다. const meta: Meta<typeof Button>과 같은 Type Annotion 방식의 경우, Primary story에서 필수 arg인 label을 누락하고 있지만, 오류가 발생하지 않습니다.

이는 satisfies로 간단하게 해결할 수 있습니다. 이제 required arg를 작성하지 않으면 TypeScript 오류가 발생합니다.

We are delighted that the TypeScript 4.9 update finally resolves the long-standing issue of not receiving warnings about missing required arguments. The satisfies operator in combination with the StoryObj type enables TypeScript to understand the connection between component and story level arguments. As a result, it is now better at displaying these warnings.

위의 문장은 Storybook 공식 블로그에서 발췌한 내용입니다. TypeScript 4.9 업데이트 덕분에 required arguments 누락에 대한 warning 문제를 해결하게 되어 기쁘다고 언급하고 있습니다.

satisfies란?

satisfies는 객체의 타입을 만족하는지 확인하는 키워드로, '미리' 유효성을 검사합니다.

Storybook 말고 또 언제 활용할 수 있을까?

1. union 타입을 사용하는 경우

type CityName = "New York" | "Mumbai" | "Lagos";
type CityCoordinates = {
  x: number;
  y: number;
};
 
type City = CityName | CityCoordinates;
 
type User = {
  birthCity: City;
  currentCity: City;
};
 
// Type Annotation을 사용한 경우
const user: User = {
  birthCity: "Mumbai",
  currentCity: { x: 6, y: 3 },
};
 
// 아래의 경우에서 타입 에러 발생
user.birthCity.toUpperCase();
 
// 에러를 없애기 위해서 아래와 같이 타입 검사하는 코드 추가
if (typeof user.birthCity === 'string') {
  user.birthCity.toUpperCase()
}
// satisfies를 사용하면?
const user = {
  birthCity: "Mumbai",
  currentCity: { x: 6, y: 3 },
} satisfies User
 
// string specific method 사용 가능
user.birthCity.toUpperCase();
 
// number specific method 사용 가능
user.currentCity.x.toFixed(2);

위의 예시 코드처럼 if(typeof something === 'type')과 같은 타입 검사를 satisfies로 대체 가능합니다. 별도 처리 없이 타입 specific method, property를 사용할 수 있다는 점에서 매우 편리해졌죠?

2. 조금 더 안전하게 타입 단언하고 싶은 경우

as와의 가장 큰 차이점은 type safety라고 할 수 있습니다. as의 문제는 개발자가 지정한 그대로 타입 단언한다는 점이죠.

// 어느 날 갑자기 User에 major property가 추가된다면?
interface User {
  name: string;
  location: string;
  major: "frontend" | "backend";
}
 
// 에러가 발생해야 맞는 상황인데 as 때문에 통과
const user = {
  name: "지연",
  location: "판교",
} as User;

satisfies는 타입 체크, as는 타입 단언을 위한 키워드입니다. 따라서 둘을 같이 사용하면 타입 체크 이후, 타입 만족 시 타입 단언을 진행합니다.

const user = {
  name: "지연",
  location: "판교",
} satisfies User as User; // ❌ Error(ts1360) major가 없어서 에러 발생

타입과 일치하는지 확인한 다음 단언하기 때문에 조금 더 안전하게 타입 단언이 가능합니다.

3. Next.js Page에서 props 타입 추론 필요한 경우

Vercel의 member Lee Robinson이 satisfies의 장점을 언급하기도 했습니다. (링크)

With the release of satisfies, you'll get improved type safety inside getStaticProps / getStaticPaths / getServerSideProps as well as inside Pages.

// 1. P는 { [key: string]: any } extends
export type GetStaticProps<
  P extends { [key: string]: any } = { [key: string]: any },
  Q extends ParsedUrlQuery = ParsedUrlQuery,
  D extends PreviewData = PreviewData
> = (
  context: GetStaticPropsContext<Q, D>
) => Promise<GetStaticPropsResult<P>> | GetStaticPropsResult<P> // 2. GetStaticPropsResult에 P를 넘김
 
export type GetStaticPropsResult<P> =
  | { props: P; revalidate?: number | boolean } // 3. props의 type은 아래에서 받아온 P
  | { redirect: Redirect; revalidate?: number | boolean }
  | { notFound: true; revalidate?: number | boolean }

GetStaticProps 타입은 위와 같아요. Generic 타입 P{[key: string]: any}를 extends 하기 때문에 Page에서 props를 추론할 때 실제 getStaticProps에서 return 하는 object의 id 속성을 추론할 수 없었습니다.

하지만 satisfies가 등장하면서 props의 타입을 정확하게 추론할 수 있게 되었습니다. 예전 작성 방식과 satisfies가 적용된 방식을 비교해 볼까요?

예전 작성 방식

export const getStaticProps: GetStaticProps = () => {
  return {
    hello: 'world'
  }
}
 
export default function Page(
  props: InferGetStaticPropsType<typeof getStaticProps>
) {
  // props type은 { [key: string]: any }
  return <div>{props.hello}</div>
}

satisfies 를 활용한 방식

export const getStaticProps = () => {
  return {
    hello: 'world'
  }
} satisfies GetStaticProps
 
export default function Page(
  props: InferGetStaticPropsType<typeof getStaticProps>
) {
  // props는 { hello: string } 타입
  return <div>{props.hello}</div>
}

바뀐 부분만 살펴보면 아래와 같습니다.

정리

  • satisfies는 객체의 타입을 만족하는지 확인하는 키워드로, 미리 유효성을 검사합니다.
  • Storybook에서 satisfies를 사용하면 required arguments 누락에 대한 warning을 받을 수 있습니다.
  • 이 외에도 union 타입을 사용하는 경우, 조금 더 안전하게 타입 단언하고 싶은 경우, Next.js Page에서 props 타입 추론이 필요한 경우에 활용할 수 있습니다.