
TypeScript satisfies 연산자 완벽 정리: 타입 체크의 새로운 강자
안녕하세요, 여러분!
오늘은 TypeScript의 satisfies
연산자가 뭔지, 어떻게 동작하는지 알아볼까요?
이 연산자는 값의 타입을 체크하면서도 (대부분) 타입에 영향을 주지 않는 멋진 기능인데요.
어디에 유용한지도 함께 살펴볼게요.
이 글에서 사용할 표기법
소스 코드에서 계산된 타입이나 추론된 타입을 보여주기 위해 asserttt
라는 npm 패키지를 사용합니다.
예를 들어:
// 값의 타입
assertType<string>('abc');
assertType<number>(123);
// 타입의 동등성
type Pair<T> = [T, T];
type _ = Assert<Equal<
Pair<string>, [string,string]
>>;
satisfies
연산자는 뭘까요?
satisfies
연산자는 컴파일 타임에서 주어진 값이 특정 타입에 할당 가능한지 강제합니다:
value satisfies Type
결과는 여전히 value
입니다.
이 연산자는 런타임에는 아무런 영향을 주지 않습니다.
보통 value
의 타입은 변하지 않는데요.
다만, 몇 가지 예외가 있으니 나중에 살펴볼게요.
문법: satisfies
앞에 줄 바꿈은 안 됩니다
아래는 허용되지 않습니다:
const sayHello = (name) => `Hello ${name}!`
// @ts-expect-error: Cannot find name 'satisfies'.
satisfies (str: string) => string;
이럴 때는 괄호를 사용하면 됩니다:
const sayHello = (
(name) => `Hello ${name}!`
) satisfies (str: string) => string;
첫 번째 예제
객체 리터럴의 타입이 다양한 어노테이션에 따라 어떻게 영향을 받는지 알아볼까요?
먼저 아무런 어노테이션 없이 시작해봅시다:
const point1 = { x: 2, y: 5 };
assertType<
{ x: number, y: number }
>(point1);
TypeScript는 .x
와 .y
의 타입을 number
로 일반화합니다.
이제 as const
어노테이션을 사용하면 어떻게 되는지 볼까요:
const point2 = { x: 2, y: 5 } as const;
assertType<
{ readonly x: 2, readonly y: 5 }
>(point2);
이제 두 속성은 읽기 전용이 되고, 타입도 좁아졌습니다.
만약 새로운 타입 Point
를 사용해 포인트의 형태가 올바른지 확인하려면 어떻게 될까요?
type Point = { x: number, y: number };
const point3: Point = { x: 2, y: 5 } as const;
assertType<
Point
>(point3);
point3
은 다시 넓은 타입을 가집니다.
TypeScript는 point1
이 가졌던 타입의 별칭인 Point
를 추론합니다.
타입 레벨 어노테이션 없이 변수를 선언했기 때문인데요.
satisfies
를 사용하면 as const
가 주는 좁은 타입을 유지하면서 포인트의 형태가 올바른지 확인할 수 있습니다:
const point4 = { x: 2, y: 5 } as const satisfies Point;
assertType<
{ readonly x: 2, readonly y: 5 }
>(point4);
satisfies
는 as
와 어떻게 다를까요?
한편으로, as
는 일반적으로 왼쪽 값의 타입을 변경합니다.
반면, satisfies
만큼 철저하게 타입을 체크하지는 않습니다:
// 누락된 속성에 대해 경고해야 하지만 하지 않습니다!
const point5 = { x: 2 } as const as Point; // OK
assertType<
Point
>(point5);
반면, satisfies
는 .y
속성이 누락된 경우 경고를 줍니다:
// @ts-expect-error: Type '{ readonly x: 2; }' does not satisfy
// the expected type 'Point'.
const point6 = { x: 2 } as const satisfies Point;
포인트의 타입을 왜 좁게 유지하고 싶을까요?
사실, 넓은 타입으로도 충분한 경우가 많습니다.
하지만 좁은 타입이 필요한 사용 사례도 있으니, 다음에 살펴볼게요.
예제: 선택적 객체 속성
아래 코드는 partialPoint1
이 넓은 타입 PartialPoint
를 가져서 작업하기 불편한 상황을 보여줍니다:
type PartialPoint = { x?: number, y?: number };
const partialPoint1: PartialPoint = { y: 7 };
// 에러가 나야 합니다
const x1 = partialPoint1.x;
assertType<number | undefined>(x1);
// 타입은 `number`여야 합니다
const y1 = partialPoint1.y;
assertType<number | undefined>(y1);
satisfies
를 사용하면 어떻게 되는지 볼까요:
const partialPoint2 = { y: 7 } satisfies PartialPoint;
// @ts-expect-error: Property 'x' does not exist on type
// '{ y: number; }'.
const x2 = partialPoint2.x;
const y2 = partialPoint2.y;
assertType<number>(y2);
객체 속성 값 타입 체크
다음 객체는 텍스트 스타일에 대한 열거형을 구현합니다:
const TextStyle = {
Bold: {
html: 'b',
latex: 'textbf',
},
Italics: {
html: 'i',
// 누락: latex
},
};
type TextStyleKeys = keyof typeof TextStyle;
type _ = Assert<Equal<
TextStyleKeys, "Bold" | "Italics"
>>;
한편으로, 속성 키를 추출해 TextStyleKeys
타입을 얻을 수 있는 점은 멋집니다.
다른 한편으로, TextStyle.Italics.latex
속성을 잊어버렸는데도 TypeScript가 경고를 주지 않았습니다.
개선: 속성 값 타입
속성 값을 체크하기 위해 다음 타입을 사용해봅시다:
type TTextStyle = {
html: string,
latex: string,
};
첫 번째 시도는 다음과 같습니다:
const TextStyle: Record<string, TTextStyle> = {
Bold: {
html: 'b',
latex: 'textbf',
},
// @ts-expect-error: Property 'latex' is missing in type
// '{ html: string; }' but required in type 'TTextStyle'.
Italics: {
html: 'i',
},
};
type TextStyleKeys = keyof typeof TextStyle;
type _ = Assert<Equal<
TextStyleKeys, string
>>;
장점: 누락된 속성에 대해 경고를 받습니다.
단점: 더 이상 속성 키를 추출할 수 없습니다 – TextStyleKeys
가 이제 string
이 됐습니다.
개선: satisfies
로 속성 값 체크
다시 한 번, satisfies
가 도움을 줄 수 있습니다:
const TextStyle = {
Bold: {
html: 'b',
latex: 'textbf',
},
// @ts-expect-error: Property 'latex' is missing in type
// '{ html: string; }' but required in type 'TTextStyle'.
Italics: {
html: 'i',
},
} satisfies Record<string, TTextStyle>;
type TextStyleKeys = keyof typeof TextStyle;
type _ = Assert<Equal<
TextStyleKeys, "Bold" | "Italics"
>>;
이제 누락된 속성에 대한 경고를 받으면서도 유용한 TextStyleKeys
타입을 얻을 수 있습니다.
객체 속성 키 타입 체크
앞의 예제에서는 속성 값의 형태를 체크했습니다.
때로는 속성 키도 제한된 집합으로 관리하고 싶을 때가 있습니다 – 오타를 방지하기 위해서 말이죠.
다음 예제는 satisfies
에 대한 풀 리퀘스트에서 영감을 얻은 것이며, 속성 키를 체크하기 위해 ColorName
타입을, 속성 값을 체크하기 위해 Color
타입을 사용합니다.
키 'blue'
가 누락돼서 에러가 발생합니다:
type ColorName = 'red' | 'green' | 'blue';
type Color =
| string // hex
| [number, number, number] // RGB
;
const fullColorTable = {
red: [255, 0, 0],
green: '#00FF00',
// @ts-expect-error:
// Type '{ red: [number, number, number]; green: string; }'
// does not satisfy the expected type 'Record<ColorName, Color>'.
} satisfies Record<ColorName, Color>;
모든 키를 사용하지 않으면서도 오타를 체크하고 싶다면 어떻게 해야 할까요?
객체의 레코드를 부분적으로 만들면 됩니다 (A 라인):
const partialColorTable = {
red: [255, 0, 0],
// @ts-expect-error: Object literal may only specify known
// properties, but 'greenn' does not exist in type
// 'Partial<Record<ColorName, Color>>'.
// Did you mean to write 'green'?
greenn: '#00FF00',
} satisfies Partial<Record<ColorName, Color>>; // (A)
속성 키를 추출할 수도 있습니다:
const partialColorTable2 = {
red: [255, 0, 0],
green: '#00FF00',
} satisfies Partial<Record<ColorName, Color>>;
type PropKeys = keyof typeof partialColorTable2;
type _ = Assert<Equal<
PropKeys, "red" | "green"
>>;
리터럴 값 제약
다음 함수 호출을 생각해봅시다:
JSON.stringify({ /*···*/ });
JSON.stringify()
의 파라미터는 any
타입을 가집니다.
우리가 전달하는 객체가 올바른 형태인지(속성 키 오타 등이 없는지) 어떻게 보장할 수 있을까요?
한 가지 방법은 객체를 변수로 만드는 것입니다:
const obj: SomeType = { /*···*/ };
JSON.stringify(obj);
또 다른 방법은 satisfies
를 사용하는 것입니다:
JSON.stringify({ /*···*/ } satisfies SomeType);
다음 하위 섹션에서는 이 기법을 여러 사용 사례에 적용해봅시다.
예제: fetch()
로 JSON 포스팅
다음 코드는 Matt Pocock의 글에서 영감을 얻은 것입니다: fetch()
를 사용해 API에 데이터를 포스팅합니다.
type Product = {
name: string,
quantity: number,
};
const response = await fetch('/api/products', {
method: 'POST',
body: JSON.stringify(
{
name: 'Toothbrush',
quantity: 3,
} satisfies Product
),
headers: {
'Content-Type': 'application/json',
},
});
예제: export default
인라인 명명된 내보내기는 타입 어노테이션을 가질 수 있습니다:
import type { StringCallback } from './core.js';
export const toUpperCase: StringCallback =
(str) => str.toUpperCase();
기본 내보내기에는 타입 어노테이션을 추가할 방법이 없지만, satisfies
를 사용할 수 있습니다:
import type { StringCallback } from './core.js';
export default (
(str) => str.toUpperCase()
) satisfies StringCallback;
satisfies
가 항상 필요한 건 아닙니다
이 섹션에서는 satisfies
가 필요할 것 같지만, TypeScript가 이미 코드를 올바르게 좁히는 경우를 살펴볼게요.
예제: 문자열과 숫자의 유니언
다음 코드에서는 str1
과 str2
에 satisfies
를 사용할 필요가 없습니다 – 이미 string
으로 좁혀져 있습니다:
type StrOrNum = string | number;
const str1 = 'abc' as StrOrNum;
// @ts-expect-error: Property 'toUpperCase' does not exist on
// type 'StrOrNum'.
str1.toUpperCase();
const str2: StrOrNum = 'abc';
str2.toUpperCase(); // OK
let str3: StrOrNum = 'abc';
str3.toUpperCase(); // OK
예제: 구별된 유니언
다음 예제에서 linkToIntro
도 구별된 유니언 LinkHref
의 한 요소로 좁혀집니다:
type LinkHref =
| {
kind: 'LinkHrefUrl',
url: string,
}
| {
kind: 'LinkHrefId',
id: string,
}
;
const linkToIntro: LinkHref = {
kind: 'LinkHrefId',
id: '#intro',
};
// 타입이 좁혀졌습니다:
assertType<
{
kind: 'LinkHrefId',
id: string,
}
>(linkToIntro);
satisfies
는 추론된 타입을 변경할 수 있습니다
satisfies
는 보통 값의 추론된 타입을 변경하지 않지만, 예외가 있습니다.
타입에서 리터럴 타입으로
type Robin = {
name: 'Robin',
};
const robin1 = { name: 'Robin' };
assertType<string>(robin1.name);
const robin2 = { name: 'Robin' } satisfies Robin;
assertType<'Robin'>(robin2.name);
배열에서 튜플로
// `satisfies` 없음
const tuple1 = ['a', 1];
assertType<
(string | number)[]
>(tuple1);
// 비어 있지 않은 튜플
const tuple2 = ['a', 1] satisfies [unknown, ...unknown[]];
assertType<
[string, number]
>(tuple2);
// 모든 튜플
const tuple3 = [] satisfies [unknown?, ...unknown[]];
assertType<
[]
>(tuple3);
// 모든 튜플
const tuple4 = ['a', 1] satisfies [] | unknown[];
assertType<
[string, number]
>(tuple4);
satisfies
는 명시적 타입을 변경하지 않습니다
const tuple1: Array<string | number> = ['a', 1];
// @ts-expect-error: Type '(string | number)[]' does not satisfy
// the expected type '[unknown, ...unknown[]]'.
const tuple2 = tuple1 satisfies [unknown, ...unknown[]];
타입 레벨 만족도 체크
TypeScript에는 satisfies
와 유사한 타입 레벨 연산이 있지만, 우리 스스로 구현할 수도 있습니다:
type Satisfies<Type extends Constraint, Constraint> = Type;
type T1 = Satisfies<123, number>; // 123
type _ = Assert<Equal<
T1, 123
>>;
// @ts-expect-error: Type 'number' does not satisfy
// the constraint 'string'.
type T2 = Satisfies<123, string>;
'Javascript' 카테고리의 다른 글
TypeScript Mapped Types 완벽 정복: 기본부터 고급 활용까지 (0) | 2025.03.13 |
---|---|
TypeScript의 infer 키워드로 복합 타입에서 원하는 부분만 깔끔하게 추출하기 (0) | 2025.03.13 |
TypeScript에서 읽기 전용 속성 완벽 정리: readonly 키워드 활용법 (0) | 2025.03.03 |
TypeScript로 구현하는 최신 ESM 기반 npm 패키지 퍼블리싱 가이드 (0) | 2025.03.03 |
JavaScript Temporal, 날짜와 시간을 다루는 새로운 방법 (0) | 2025.03.02 |