- Published on
TS 5.5+ 인라인 타입 프레딕트로 추론 고치기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 코드든 프론트 코드든, undefined 나 null 이 섞인 배열을 정리하는 일은 흔합니다. 문제는 TypeScript가 런타임에서 안전한 필터링을 했는데도 타입 레벨에서는 여전히 “가능성”을 남겨두는 경우가 많다는 점입니다. 그 결과, filter(Boolean) 같은 관용구가 타입을 충분히 좁히지 못해 불필요한 타입 단언이 늘어나고, 진짜 위험한 단언이 섞이면서 버그가 잠복합니다.
TypeScript 5.5+의 인라인 타입 프레딕트(inline type predicate) 는 이런 상황에서 “추론이 무너진 지점”을 적은 코드로 복구할 수 있게 해줍니다. 특히 Array.prototype.filter 콜백에서 빛을 발합니다.
이번 글에서는 TS 5.5+에서 무엇이 달라졌는지, 어떤 패턴이 실무에서 가장 효과적인지, 그리고 남아 있는 함정까지 정리합니다.
왜 기존 패턴은 추론이 자주 깨질까
대표적인 예로 API 응답을 파싱하고 유효한 값만 남기는 흐름을 생각해봅시다.
const raw: Array<string | undefined> = ["a", undefined, "b"];
const cleaned = raw.filter(Boolean);
// 기대: string[]
// 현실: (string | undefined)[] 인 경우가 많음
Boolean 은 런타임에서 truthy / falsy 를 판정하지만, 타입 시스템 관점에서 Boolean 은 “무슨 타입을 좁힌다”라는 의미를 갖지 않습니다. 즉, filter(Boolean) 는 사람이 보기엔 명확해도 컴파일러에겐 정보가 부족합니다.
그래서 실무에서는 다음 같은 우회가 흔했습니다.
- 커스텀 타입 가드 함수 만들기
as단언으로 밀어붙이기(위험)flatMap으로[]또는[value]패턴 사용
TS 5.5+ 인라인 타입 프레딕트는 “커스텀 타입 가드 함수를 따로 만들지 않고도” 콜백 자리에서 타입 좁힘을 선언할 수 있게 해줍니다.
TS 5.5+ 인라인 타입 프레딕트란
기존에도 타입 프레딕트는 있었습니다.
function isDefined<T>(x: T | undefined | null): x is T {
return x !== undefined && x !== null;
}
TS 5.5+에서 편해진 포인트는, 이런 타입 프레딕트를 함수 선언으로 분리하지 않고 콜백 위치에서 바로 쓸 수 있다는 점입니다.
핵심 문법은 다음 형태입니다.
(param): param is SomeType => booleanExpression
즉, 반환 타입 자리에 param is ... 를 적어 “이 조건이 참이면 param 은 이 타입이다”를 인라인으로 표현합니다.
filter 에서 가장 많이 쓰는 3가지 패턴
1) undefined / null 제거: x is NonNullable<T>
가장 흔한 케이스입니다.
const raw: Array<string | undefined | null> = ["a", null, undefined, "b"];
const cleaned = raw.filter(
(x): x is NonNullable<typeof x> => x != null
);
// cleaned: string[]
포인트:
x != null은null과undefined둘 다 제거하는 관용구입니다.NonNullable로 결과 타입을 깔끔하게 만듭니다.typeof x를 쓰면 “현재 요소 타입”을 기반으로 자동 추론되므로 제네릭을 직접 꺼내지 않아도 됩니다.
2) 유니온에서 특정 브랜치만 남기기
예를 들어 파서가 다양한 형태를 반환하고, 그중 성공 케이스만 남기고 싶다고 해봅시다.
type ParseOk = { ok: true; value: number };
type ParseErr = { ok: false; error: string };
type ParseResult = ParseOk | ParseErr;
const results: ParseResult[] = [
{ ok: true, value: 1 },
{ ok: false, error: "bad" },
{ ok: true, value: 2 },
];
const oks = results.filter(
(r): r is ParseOk => r.ok
);
// oks: ParseOk[]
// oks[0].value 접근이 안전해짐
이 패턴은 discriminated union 과 궁합이 좋고, if (r.ok) 를 매번 쓰지 않고도 이후 파이프라인 전체가 단순해집니다.
3) “truthy” 필터링을 타입 안전하게 만들기
filter(Boolean) 을 대체하고 싶지만, 단순히 x != null 로는 부족할 때가 있습니다. 예를 들어 빈 문자열 "" 을 제거하고 싶을 수도 있습니다.
const xs: Array<string | undefined> = ["a", "", undefined, "b"];
const nonEmpty = xs.filter(
(x): x is string => typeof x === "string" && x.length > 0
);
// nonEmpty: string[] 이면서, 런타임에서도 빈 문자열이 제거됨
이처럼 “무엇을 제거할지”를 명시적으로 표현하면, 런타임 의미와 타입 의미가 정확히 일치합니다.
실무 예제: map 후 filter 에서 추론 복구하기
많이 겪는 형태가 map 단계에서 undefined 를 만들고, 다음 단계에서 제거하는 파이프라인입니다.
type User = { id: string; email?: string };
const users: User[] = [
{ id: "1", email: "a@example.com" },
{ id: "2" },
{ id: "3", email: "c@example.com" },
];
const emails = users
.map((u) => u.email)
.filter((e): e is string => e !== undefined);
// emails: string[]
여기서 filter((e) => e !== undefined) 만 쓰면 TS가 상황에 따라 string | undefined 를 유지하는 경우가 있는데, 인라인 타입 프레딕트를 붙이면 결과가 안정적으로 string[] 으로 고정됩니다.
인라인 타입 프레딕트 사용 시 주의할 점
1) 타입 프레딕트는 “약속”이다: 런타임 조건과 반드시 일치해야 함
다음은 컴파일은 되지만 위험한 예입니다.
const nums: Array<number | string> = [1, "2", 3];
const onlyNumbers = nums.filter(
(x): x is number => true
);
// onlyNumbers 는 number[] 로 보이지만 실제로는 string 이 섞임
타입 프레딕트는 컴파일러에게 강한 힌트를 주는 대신, 개발자가 조건을 정확히 써야 합니다. 불일치하면 타입 안정성이 아니라 “타입 위조”가 됩니다.
2) NonNullable<typeof x> 와 NonNullable<T> 중 무엇을 쓸까
- 콜백 파라미터
x가 이미 적절한 유니온 타입을 갖고 있다면NonNullable<typeof x>가 간결합니다. - 제네릭 유틸 함수로 빼서 재사용하려면
NonNullable<T>형태가 더 자연스럽습니다.
재사용 버전 예시는 아래와 같습니다.
export function isNonNullable<T>(x: T): x is NonNullable<T> {
return x != null;
}
const cleaned = ["a", undefined, "b"].filter(isNonNullable);
// cleaned: string[]
인라인이든 분리든, 팀 스타일과 재사용성에 맞춰 선택하면 됩니다.
3) filter 말고도 find / every / some 에서도 생각해보기
인라인 타입 프레딕트는 filter 에서 가장 체감이 크지만, “조건이 참이면 이 타입이다”라는 모델이 맞는 곳이라면 다른 배열 메서드에서도 유용합니다.
다만 find 는 결과가 단일 값이므로 undefined 처리가 여전히 필요합니다.
type Item = { kind: "a"; a: number } | { kind: "b"; b: string };
const items: Item[] = [{ kind: "b", b: "x" }, { kind: "a", a: 1 }];
const foundA = items.find(
(it): it is Extract<Item, { kind: "a" }> => it.kind === "a"
);
// foundA: { kind: "a"; a: number } | undefined
어떤 상황에서 특히 효과가 큰가
- DTO 파싱 / 검증 이후 “유효한 것만 남기기”
- 이벤트 스트림에서 특정 타입 이벤트만 집계
Promise.allSettled결과에서 fulfilled 만 추출- GraphQL / REST 응답에서 부분적으로 누락된 필드 정리
이런 종류의 문제는 운영 장애처럼 크게 터지기보다, “타입 단언이 누적되며 유지보수가 악화”되는 형태로 나타납니다. 장애 대응 글에서 자주 말하는 것처럼, 원인을 빨리 좁히려면(진단 비용을 낮추려면) 시스템이 스스로 더 많은 힌트를 줘야 합니다. 타입 시스템도 마찬가지입니다. 추론이 흔들리는 곳을 인라인 타입 프레딕트로 고정하면, 이후 코드는 더 적은 방어 로직으로도 안전해집니다.
관심 있다면 운영 환경에서의 “진단 비용을 낮추는” 접근을 다른 관점에서 다룬 글로 Go 고루틴 leak 추적 - pprof·trace로 10분 진단 도 함께 읽어볼 만합니다.
Promise.allSettled 실전 예제로 보는 추론 개선
Promise.allSettled 는 결과가 fulfilled 와 rejected 유니온이라 후처리가 번거롭습니다.
const tasks = [
Promise.resolve(1),
Promise.reject(new Error("fail")),
Promise.resolve(3),
];
const settled = await Promise.allSettled(tasks);
const values = settled
.filter(
(r): r is PromiseFulfilledResult<number> => r.status === "fulfilled"
)
.map((r) => r.value);
// values: number[]
여기서 인라인 타입 프레딕트가 없으면 map 단계에서 value 접근이 막히거나, 단언이 필요해집니다. 반면 위 코드는 런타임 조건과 타입 조건이 정확히 일치합니다.
TS 설정과 린트 관점 팁
strict모드에서 효과가 더 크게 체감됩니다.- ESLint 사용 시, 불필요한 타입 단언을 줄이기 위해
@typescript-eslint/no-unnecessary-type-assertion같은 룰과 함께 쓰면 좋습니다. - 팀 컨벤션으로
filter(Boolean)사용을 제한하고, 인라인 타입 프레딕트 또는isNonNullable같은 공용 가드를 쓰도록 합의하면 코드베이스 일관성이 올라갑니다.
마무리
TS 5.5+ 인라인 타입 프레딕트는 “타입 가드 함수를 굳이 밖으로 빼지 않아도 되는” 작은 문법 개선처럼 보이지만, 실제로는 배열 파이프라인에서 타입 추론을 안정화하는 강력한 도구입니다.
정리하면:
filter(Boolean)같은 관용구는 런타임 의미와 타입 의미가 어긋날 수 있습니다.- 인라인 타입 프레딕트로
filter단계에서 결과 타입을 명확히 고정할 수 있습니다. - 조건과 프레딕트 타입은 반드시 일치해야 하며, 그렇지 않으면 타입 안정성을 스스로 깨게 됩니다.
대규모 코드베이스에서 “불필요한 단언 제거”는 단순한 미관 문제가 아니라, 장애 가능성을 낮추고 변경 비용을 줄이는 작업입니다. 인라인 타입 프레딕트를 자주 깨지는 파이프라인부터 적용해보면 효과가 빠르게 보일 것입니다.
추가로, 재시도/백오프처럼 실패 케이스를 명확히 다루는 패턴에 관심이 있다면 OpenAI 429 Rate Limit 재시도·백오프 구현 가이드 도 같은 맥락에서 도움이 됩니다.