개발 기록 남기기✍️

[TypeScript] TypeScript satisfies 연산자 본문

Front-End/TypeScript

[TypeScript] TypeScript satisfies 연산자

너해동물원 2024. 4. 29. 23:14

Next.js 공식문서에서 getServerSideProps를 보다가 문득..

// pages/index.ts

import type { InferGetServerSidePropsType, GetServerSideProps } from 'next'

type Repo = {
  name: string
  stargazers_count: number
}

export const getServerSideProps = (async () => {
  ...
}) satisfies GetServerSideProps<{ ... }> // 🔥

 

satisfies라는 키워드가 보이더군요. 흐음?

게다가 Vercel의 구성원 중 한 명인 Lee Robinson은 다음과 같이 말합니다.

 

TypeScript 4.9는 Next.js에 거대한 영향을 미칠 것입니다. satisfies를 통해서 더 향상된 타입 안전성을 가져갈 수 있습니다.

 

아니 도대체 satisfies가 뭐길래 이렇게 난리란 말임.. 🤔 아직까지는 그렇게 필요성을 못 느끼겠는뎁쇼…

한번 파헤쳐볼까요~? 🔎

 

 

TypeScript 4.9 그리고 satisfies

TypeScript 공식문서에서는 satisfies에 대해 다음과 같이 설명하고 있습니다.

satisfies 연산자를 사용하면 표현식의 결과 타입을 변경하지 않고 표현식의 타입이 특정 타입과 일치하는지 검증할 수 있습니다.

satisfies는 말 그대로 변수의 타입이 type, interface만족하는지 확인한다는 의미입니다.

satisfies 연산자는 타입스크립트에게 satisfies 뒤에 오는 타입을 모두 찾아 미리 검증(prevalidating)하라고 강제합니다.

말로만 풀어내면 어려우니, 공식문서 예시를 보며 이해해보도록 합니다.

 

type Colors = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];

type Palette = Record<Colors, string | RGB>;

const palette: Palette = {
        red: [255, 0, 0],
    green: "#00ff00",
    blue: [0, 0, 255]
};

 

palette 객체의 각 프로퍼티는 string 혹은 RGB일 수 있습니다.

palette의 타입이 Palette라고 선언했을 때, 객체의 key와 value의 타입이 타입을 충족하는지를 검사하여 올바르지 않은 속성 할당을 방지할 수 있습니다.

 

하지만 다음과 같은 문제가 발생하게 됩니다.

const greenComponent = palette.green.toUpperCase();
// 🚨 Property 'toUpperCase' does not exist on type 'string | RGB'.
//    Property 'toUpperCase' does not exist on type 'RGB'.

 

우리는 green의 값을 string 형태로 작성해줘서 toUpperCase 메서드가 당연히 실행될 것을 기대했는데, green의 프로퍼티 타입은 여전히 string | RGB인 것을 확인할 수 있습니다.

때문에 기존 타입스크립트 환경에서는 아래와 같은 검증을 거쳐야 했습니다.

const getGreenComponent = (palette:Palette) => {
    if (typeof palette.green === string) {
        palette.green.toUpperCase(); // ✅
    }
}

 

satisfies 연산자를 사용하면 유니온 타입과 호환되는 타입의 메서드 호출 시도 시 에러를 발생시키지 않습니다.

 

type Colors = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];

type Palette = Record<Colors, string | RGB>;

const palette = {
        red: [255, 0, 0],
    green: "#00ff00",
    blue: [0, 0, 255]
} satisfies Palette;

const greenComponent = palette.green.toUpperCase(); // ✅

 

satisfies Palette를 적용시킨 palette의 타입을 한번 확인해볼까요?

 

 

호오.. 원하는 대로 타입이 잘 좁혀진 것을 확인할 수 있습니다.

 

 

🙋🏻‍♀️ 근데 as const랑 다를게 뭐가 있쬬?

type Colors = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];

type Palette = Record<Colors, string | RGB>;

const palette = {
        red: [255, 0, 0],
    green: "#00ff00",
    blue: [0, 0, 255]
} as const;

const func1 = (val:Palette) => {
  console.log(val)
}

func1(palette);
// 🚨 Argument of type '{ readonly red: readonly [255, 0, 0]; readonly green: "#00ff00"; readonly blue: readonly [0, 0, 255]; }' is not assignable to parameter of type 'Palette'.

 

구체적인 타입을 추론하기 위해서 보통 as const를 많이 사용하는데, as const를 사용해서 타입을 추론할 경우 작성한 프로퍼티로 타입을 좁혀주게 됩니다. 따라서 Palette 타입과는 호환성이 맞지 않게되어 Palette 타입의 변수 혹은 인수에 할당할 시 에러가 발생하게 됩니다.

 

해당 변수를 readonly 속성으로 동결함과 동시에 Palette 타입을 할당하고 싶은 경우에는 다음과 같이 사용하면 타입 에러 문제를 해결할 수 있습니다.

const palette = {
        red: [255, 0, 0],
    green: "#00ff00",
    blue: [0, 0, 255]
} as const satisfies Palette;

 

이제 TypeScript는 palette의 프로퍼티에 다른 값을 할당할 수 없음과 동시에 Palette 타입을 만족시키도록 합니다. 😉

 

 

🔎 satisfies 좀 더 파헤쳐보기

 

아래의 코드를 볼까요?

const deviceState = {
  hasMacbook : true,
  hasIPad : true,
  hasIPhone : true,
  hasMac : false
} satisfies Record<string, boolean>;

deviceState.hasMac = true;
// 🚨 Type 'true' is not assignable to type 'false'.

 

deviceState.hasMac의 타입은 boolean이 아닌 false이기 때문에 true 값을 바인딩할 경우, 타입 에러가 발생합니다.

 

따라서 deviceState가 정적 타입 체크에서 freeze object가 아닌 것으로 간주하기 위해서는 다음과 같이 타입을 지정해줄 수 있습니다.

const checkDevice = {
  hasMacbook : true,
  hasIPad : true,
  hasIPhone : true,
  hasMac : false
} satisfies Record<string, unknown>;

 

value의 타입으로 unknown을 지정해줬더니 TypeScript가 value의 타입을 추론해주는 것을 볼 수 있습니다.

 

 

🏃‍♀️ Next.js, TypeScript 그리고 satisfies 레츠고

이제 satisfies 연산자가 어떤 역할을 하는지 살펴봤으니 맨 처음에 보았던 코드를 다시 살펴봅시다.

const getServerSideProps = (async (ctx) => {
  return {
    props: {
      session: true,
      db: 'prisma'
    },
  }
}) satisfies GetServerSideProps

type Props = InferGetServerSidePropsType<typeof getServerSideProps>

 

 

GetServierSideProps는 다음과 같은 타입을 가집니다.

export type GetServerSideProps<
  Props extends { [key: string]: any } = { [key: string]: any },
  Params extends ParsedUrlQuery = ParsedUrlQuery,
  Preview extends PreviewData = PreviewData
> = (
  context: GetServerSidePropsContext<Params, Preview>
) => Promise<GetServerSidePropsResult<Props>>

 

TypeScript 4.9 버전 이전에는 GetServerSideProps에서 제네릭 타입 P{[key: string]: any}를 확장하기 때문에 Page 컴포넌트에서 props의 타입을 추론할 때 실제 getServerSideProps에서 리턴하는 object의 프로퍼티 속성을 추론하지 못하는 문제가 있었습니다.

 

satisfies를 사용하면 GetServerSideProps의 타입을 가짐과 동시에 props의 실제 타입을 추론하게 됩니다.

 

Props의 타입은 다음과 같이 추론됩니다.

type Props = {
    session: boolean;
    db: string;
}

 

고마워요 따봉 satisfies 연산자!

따봉