Fix TypeScript type narrowing issue with isUrlString

Fixes #212
This commit is contained in:
Sindre Sorhus 2025-09-12 03:58:08 +07:00
parent ef35cc350a
commit c68ad76062
3 changed files with 33 additions and 3 deletions

View file

@ -8,6 +8,7 @@ import type {
Predicate, Predicate,
Primitive, Primitive,
TypedArray, TypedArray,
UrlString,
WeakRef, WeakRef,
Whitespace, Whitespace,
} from './types.js'; } from './types.js';
@ -760,7 +761,7 @@ export function isUrlSearchParams(value: unknown): value is URLSearchParams {
return getObjectType(value) === 'URLSearchParams'; return getObjectType(value) === 'URLSearchParams';
} }
export function isUrlString(value: unknown): value is string { export function isUrlString(value: unknown): value is UrlString {
if (!isString(value)) { if (!isString(value)) {
return false; return false;
} }
@ -894,7 +895,7 @@ type Assert = {
dataView: (value: unknown, message?: string) => asserts value is DataView; dataView: (value: unknown, message?: string) => asserts value is DataView;
enumCase: <T = unknown>(value: unknown, targetEnum: T, message?: string) => asserts value is T[keyof T]; enumCase: <T = unknown>(value: unknown, targetEnum: T, message?: string) => asserts value is T[keyof T];
urlInstance: (value: unknown, message?: string) => asserts value is URL; 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: <T>(value: T | Falsy, message?: string) => asserts value is T; truthy: <T>(value: T | Falsy, message?: string) => asserts value is T;
falsy: (value: unknown, message?: string) => asserts value is Falsy; falsy: (value: unknown, message?: string) => asserts value is Falsy;
nan: (value: unknown, message?: string) => asserts value is number; 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)) { if (!isUrlString(value)) {
throw new TypeError(message ?? typeErrorMessage('string with a URL', value)); throw new TypeError(message ?? typeErrorMessage('string with a URL', value));
} }
@ -1705,4 +1706,5 @@ export type {
Predicate, Predicate,
Primitive, Primitive,
TypedArray, TypedArray,
UrlString,
} from './types.js'; } from './types.js';

View file

@ -75,3 +75,10 @@ export type Predicate = (value: unknown) => boolean;
export type NonEmptyString = string & {0: string}; export type NonEmptyString = string & {0: string};
export type Whitespace = ' '; 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'};

View file

@ -17,6 +17,7 @@ import is, {
type Primitive, type Primitive,
type TypedArray, type TypedArray,
type TypeName, type TypeName,
type UrlString,
} from '../source/index.js'; } from '../source/index.js';
import {keysOf} from '../source/utilities.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<UrlString>();
expectTypeOf(value).toMatchTypeOf<string>();
} else {
// ✅ In false branch: value remains unknown (not incorrectly narrowed)
expectTypeOf(value).toEqualTypeOf<unknown>();
// ✅ Manual narrowing to string still works
if (typeof value === 'string') {
expectTypeOf(value).toEqualTypeOf<string>();
}
}
})();
test('is.truthy', t => { test('is.truthy', t => {
t.true(is.truthy('unicorn')); t.true(is.truthy('unicorn'));
t.true(is.truthy('🦄')); t.true(is.truthy('🦄'));