#25747β€’Hard

IsNegativeNumber

IsNegativeNumber<N> returns true for negative literals, false for zero and positives, and never for number or unions. Union detection is the tricky part.

Checking the minus sign is easy. Recognizing that the input is a union is the actual puzzle.

To rule out (or enforce) that a numeric literal is positive, you first need a way to tell whether it's negative. IsNegativeNumber is a type-level function that accepts a number N and returns:

The sign check itself is one line. The interesting parts are detecting the wide number type and detecting union inputs, two guard patterns you'll reuse whenever a type utility only makes sense for a single concrete literal.

Challenge Instructions: IsNegativeNumber

Hard

Sometimes when working with numeric literals, we need to rule out (or enforce) that the provided number is a positive integer.

To do that, we first need a way to tell if the number is negative.

Write a type-level function IsNegativeNumber that accepts a number N and returns:

  • true if N is negative
  • false if N is positive
  • false if N is 0,
  • never if N is number
  • never if N is a union

View on GitHub: https://tsch.js.org/25747

Change the following code to make the test cases pass (no type check errors).

πŸ‘‹ Lifetime-License is leaving on August 10, 2026

Get it now for $29

One-time payment. Lifetime access to all pro challenges.

Loading...

Detailed Explanation

The solution stacks three guards in front of a one-line check:

type IsUnion<T, U = T> = T extends U ? ([U] extends [T] ? false : true) : never
 
type IsNegativeNumber<T extends number> = number extends T
  ? never
  : IsUnion<T> extends true
    ? never
    : `${T}` extends `-${string}`
      ? true
      : false

Reject number, reject unions, then read the sign. In that order.

Guard 1: is it the wide number type?

The constraint T extends number already guarantees every input is assignable to number. The reverse question, number extends T, is only true when T is number itself: no literal like -1 or 0 can absorb the entire number type. So this line singles out exactly the IsNegativeNumber<number> case and returns never, as the challenge demands. Without this guard, IsNegativeNumber<number> would fall through to the sign check, and since `${number}` doesn't match `-${string}`, it would wrongly return false instead of never. A wide number has no knowable sign, so we bail out before ever trying to inspect one.

Guard 2: is it a union?

IsUnion is a classic idiom built from two mechanics:

Walk through IsUnion<-1 | -2>. Distribution runs the body twice: once with T = -1 and once with T = -2, but U stays -1 | -2 both times. Is [-1 | -2] assignable to [-1]? No, the union is wider than the member, so each pass yields true:

IsUnion<-1 | -2> // true | true β†’ true
IsUnion<-1>      // [-1] extends [-1] β†’ false

For a single literal, the member and the whole are identical, so the tuple check succeeds and the result is false. In short: a type is a union exactly when it's strictly wider than each of its own members. When IsUnion<T> is true, we return never. (The trailing : never inside IsUnion itself is unreachable, since every distributed member extends the union it came from; it's just the mandatory false branch of the conditional.)

You might wonder why IsNegativeNumber doesn't just distribute over the union and return true for each member. That's exactly the behavior the challenge rules out: IsNegativeNumber<-1 | -2> must be never, not true. So we detect the union explicitly instead of letting distribution happen.

Guard 3: reading the sign

With wide types and unions out of the way, T is a single numeric literal. Stringify it with a template literal and pattern-match on the first character:

[object Object]

`${-1.9}` evaluates to the string literal '-1.9', which matches `-${string}` β†’ true. `${0}` is '0' and `${1.9}` is '1.9', no leading minus β†’ false. There's no type-level arithmetic or comparison happening anywhere; the minus sign in the literal's text is all we need.

Edge cases the tests cover

Both guard patterns, number extends T for wideness and the tuple-wrapped IsUnion, show up constantly in production type utilities. That's what earns this small challenge its "hard" label.

This challenge is originally from here.

Share this challenge

Learn the Concepts