From c68ad76062eafbd384badd7e96d09776307478c8 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Fri, 12 Sep 2025 03:58:08 +0700 Subject: [PATCH] Fix TypeScript type narrowing issue with `isUrlString` Fixes #212 --- source/index.ts | 8 +++++--- source/types.ts | 7 +++++++ test/test.ts | 21 +++++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/source/index.ts b/source/index.ts index 894318e..b89c90e 100644 --- a/source/index.ts +++ b/source/index.ts @@ -8,6 +8,7 @@ import type { Predicate, Primitive, TypedArray, + UrlString, WeakRef, Whitespace, } from './types.js'; @@ -760,7 +761,7 @@ export function isUrlSearchParams(value: unknown): value is URLSearchParams { return getObjectType(value) === 'URLSearchParams'; } -export function isUrlString(value: unknown): value is string { +export function isUrlString(value: unknown): value is UrlString { if (!isString(value)) { return false; } @@ -894,7 +895,7 @@ type Assert = { dataView: (value: unknown, message?: string) => asserts value is DataView; enumCase: (value: unknown, targetEnum: T, message?: string) => asserts value is T[keyof T]; urlInstance: (value: unknown, message?: string) => asserts value is URL; - urlString: (value: unknown, message?: string) => asserts value is string; + urlString: (value: unknown, message?: string) => asserts value is UrlString; truthy: (value: T | Falsy, message?: string) => asserts value is T; falsy: (value: unknown, message?: string) => asserts value is Falsy; nan: (value: unknown, message?: string) => asserts value is number; @@ -1650,7 +1651,7 @@ export function assertUrlSearchParams(value: unknown, message?: string): asserts } } -export function assertUrlString(value: unknown, message?: string): asserts value is string { +export function assertUrlString(value: unknown, message?: string): asserts value is UrlString { if (!isUrlString(value)) { throw new TypeError(message ?? typeErrorMessage('string with a URL', value)); } @@ -1705,4 +1706,5 @@ export type { Predicate, Primitive, TypedArray, + UrlString, } from './types.js'; diff --git a/source/types.ts b/source/types.ts index b79a603..9ad9f2e 100644 --- a/source/types.ts +++ b/source/types.ts @@ -75,3 +75,10 @@ export type Predicate = (value: unknown) => boolean; export type NonEmptyString = string & {0: string}; export type Whitespace = ' '; + +/** +A string that represents a valid URL. + +This is a branded type to prevent incorrect TypeScript type narrowing. +*/ +export type UrlString = string & {readonly __brand: 'UrlString'}; diff --git a/test/test.ts b/test/test.ts index bb09f37..32e6c1a 100644 --- a/test/test.ts +++ b/test/test.ts @@ -17,6 +17,7 @@ import is, { type Primitive, type TypedArray, type TypeName, + type UrlString, } from '../source/index.js'; import {keysOf} from '../source/utilities.js'; @@ -757,6 +758,26 @@ test('is.urlString', t => { }); }); +// Type test for urlString narrowing fix (issue #212) +// This test demonstrates that the fix allows proper type narrowing in both branches +(() => { + const value: unknown = 'test'; + + if (is.urlString(value)) { + // ✅ In true branch: value is narrowed to UrlString + expectTypeOf(value).toEqualTypeOf(); + expectTypeOf(value).toMatchTypeOf(); + } else { + // ✅ In false branch: value remains unknown (not incorrectly narrowed) + expectTypeOf(value).toEqualTypeOf(); + + // ✅ Manual narrowing to string still works + if (typeof value === 'string') { + expectTypeOf(value).toEqualTypeOf(); + } + } +})(); + test('is.truthy', t => { t.true(is.truthy('unicorn')); t.true(is.truthy('🦄'));