From 55557e14760e43ef5da73ad343412513af3922e5 Mon Sep 17 00:00:00 2001 From: benzaria Date: Sun, 25 May 2025 14:58:46 +0100 Subject: [PATCH 1/9] `IsLiteral`: Adding strict options and Fixing unpredictable behaviors of `IsStringLiteral` returning boolean for unions and `IsNumericLiteral` returning false for numeric unions - Matching `IsStringLiteral` behavior to the other is*Literals when dealing with unions of different types return boolean, now return false. - Fixing `IsNumericLiteral` return false for numeric union like (1 | 1n), now return true. - Adding `strict` option to control the behavior of `IsStringLiteral` against infinite signature types. --- source/internal/type.d.ts | 30 ++++++-- source/is-literal.d.ts | 108 ++++++++++++++++++--------- test-d/is-literal.ts | 151 +++++++++++++++++++++++++------------- 3 files changed, 199 insertions(+), 90 deletions(-) diff --git a/source/internal/type.d.ts b/source/internal/type.d.ts index 85dac83ff..07aae1e81 100644 --- a/source/internal/type.d.ts +++ b/source/internal/type.d.ts @@ -1,7 +1,8 @@ -import type {If} from '../if.d.ts'; -import type {IsAny} from '../is-any.d.ts'; -import type {IsNever} from '../is-never.d.ts'; import type {Primitive} from '../primitive.d.ts'; +import type {IsNever} from '../is-never.d.ts'; +import type {IsAny} from '../is-any.d.ts'; +import type {And} from '../and.d.ts'; +import type {If} from '../if.d.ts'; /** Matches any primitive, `void`, `Date`, or `RegExp` value. @@ -13,6 +14,13 @@ Matches non-recursive types. */ export type NonRecursiveType = BuiltIns | Function | (new (...arguments_: any[]) => unknown); +/** + * Checks if one type extends another. Note: this is not quite the same as `Left extends Right` because: + * 1. If either type is `never`, the result is `true` iff the other type is also `never`. + * 2. Types are wrapped in a 1-tuple so that union types are not distributed - instead we consider `string | number` to _not_ extend `number`. If we used `Left extends Right` directly you would get `Extends` => `false | true` => `boolean`. + */ +export type Extends = IsNever extends true ? IsNever : [Left] extends [Right] ? true : false; + /** Returns a boolean for whether the two given types extends the base type. */ @@ -40,9 +48,19 @@ export type HasMultipleCallSignatures unknow : false; /** -Returns a boolean for whether the given `boolean` is not `false`. +Returns a boolean for whether the given `boolean` Union containe's `false`. +*/ +export type IsNotFalse = Not>; + +/** +Returns a boolean for whether the given `boolean` Union members are all `true`. +*/ +export type IsTrue = Extends; + +/** +Returns a boolean for whether the given `boolean` Union members are all `false`. */ -export type IsNotFalse = [T] extends [false] ? false : true; +export type IsFalse = Extends; /** Returns a boolean for whether the given type is primitive value or primitive type. @@ -59,7 +77,7 @@ IsPrimitive //=> false ``` */ -export type IsPrimitive = [T] extends [Primitive] ? true : false; +export type IsPrimitive = Extends; /** Returns a boolean for whether A is false. diff --git a/source/is-literal.d.ts b/source/is-literal.d.ts index 99473810c..e903fc76e 100644 --- a/source/is-literal.d.ts +++ b/source/is-literal.d.ts @@ -1,8 +1,22 @@ +import type {ApplyDefaultOptions, CollapseLiterals} from './internal/object.d.ts'; +import type {Extends, IsNotFalse, IsTrue, Not} from './internal/type.d.ts'; +import type {TagContainer, UnwrapTagged} from './tagged.d.ts'; import type {Primitive} from './primitive.d.ts'; -import type {Numeric} from './numeric.d.ts'; -import type {CollapseLiterals, IfNotAnyOrNever, IsNotFalse, IsPrimitive} from './internal/index.d.ts'; import type {IsNever} from './is-never.d.ts'; -import type {TagContainer, UnwrapTagged} from './tagged.d.ts'; +import type {Numeric} from './numeric.d.ts'; +import type {IsAny} from './is-any.d.ts'; +import type {And} from './and.d.ts'; + +/** +@see {@link IsLiteral} +*/ +type IsLiteralOptions = { + strict?: boolean; +}; + +type DefaultIsLiteralOptions = { + strict: true; +}; /** Returns a boolean for whether the given type `T` is the specified `LiteralType`. @@ -24,11 +38,10 @@ LiteralCheck<1, string> type LiteralCheck = ( IsNever extends false // Must be wider than `never` ? [T] extends [LiteralType & infer U] // Remove any branding - ? [U] extends [LiteralType] // Must be narrower than `LiteralType` - ? [LiteralType] extends [U] // Cannot be wider than `LiteralType` - ? false - : true - : false + ? And< + Extends, // Must be narrower than `LiteralType` + Not> // Cannot be wider than `LiteralType` + > : false : false ); @@ -47,12 +60,16 @@ LiteralChecks<1n, Numeric> LiteralChecks //=> false ``` + +@deprecated + */ type LiteralChecks = ( // Conditional type to force union distribution. // If `T` is none of the literal types in the union `LiteralUnionType`, then `LiteralCheck` will evaluate to `false` for the whole union. // If `T` is one of the literal types in the union, it will evaluate to `boolean` (i.e. `true | false`) - IsNotFalse : never > @@ -111,21 +128,31 @@ type L2 = Length<`${number}`>; //=> number ``` +@see IsStringPrimitive @category Type Guard @category Utilities */ -export type IsStringLiteral = IfNotAnyOrNever ? UnwrapTagged : S>>, -false, false>; - -export type _IsStringLiteral = -// If `T` is an infinite string type (e.g., `on${string}`), `Record` produces an index signature, -// and since `{}` extends index signatures, the result becomes `false`. -S extends string - ? {} extends Record - ? false - : true - : false; +export type IsStringLiteral = ( + ApplyDefaultOptions extends infer ResolvedOptions extends Required + ? IsNever extends false + ? CollapseLiterals ? UnwrapTagged : T> extends infer Type + ? ResolvedOptions['strict'] extends true + ? IsTrue<_IsStringLiteral> + : LiteralCheck + : never + : false + : never +); + +type _IsStringLiteral = ( + // If `T` is an infinite string type (e.g., `on${string}`), `Record` produces an index signature, + // and since `{}` extends index signatures, the result becomes `false`. + S extends string + ? {} extends Record + ? false + : true + : false +); /** Returns a boolean for whether the given type is a `number` or `bigint` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). @@ -170,10 +197,17 @@ endsWith('abc123', end); //=> boolean ``` +@see IsNumericPrimitive @category Type Guard @category Utilities */ -export type IsNumericLiteral = LiteralChecks; +export type IsNumericLiteral = IsTrue< +T extends number + ? T extends bigint + ? LiteralCheck + : LiteralCheck + : LiteralCheck +>; /** Returns a boolean for whether the given type is a `true` or `false` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). @@ -210,6 +244,7 @@ const eitherId = getId({asString: runtimeBoolean}); //=> number | string ``` +@see IsBooleanPrimitive @category Type Guard @category Utilities */ @@ -245,14 +280,15 @@ get({[symbolValue]: 1} as const, symbolValue); //=> number ``` +@see IsSymbolPrimitive @category Type Guard @category Utilities */ export type IsSymbolLiteral = LiteralCheck; /** Helper type for `IsLiteral`. */ -type IsLiteralUnion = - | IsStringLiteral +type IsLiteralUnion = + | IsStringLiteral | IsNumericLiteral | IsBooleanLiteral | IsSymbolLiteral; @@ -270,13 +306,13 @@ import type {IsLiteral} from 'type-fest'; // https://github.com/inocan-group/inferred-types/blob/master/src/types/string-literals/StripLeading.ts export type StripLeading = - A extends string - ? B extends string - ? IsLiteral extends true - ? string extends B ? never : A extends `${B & string}${infer After}` ? After : A - : string - : A - : A; +A extends string + ? B extends string + ? IsLiteral extends true + ? string extends B ? never : A extends `${B & string}${infer After}` ? After : A + : string + : A + : A; function stripLeading(input: Input, strip: Strip) { return input.replace(`^${strip}`, '') as StripLeading; @@ -291,10 +327,12 @@ stripLeading(str, 'abc'); //=> string ``` +@see IsPrimitive @category Type Guard @category Utilities */ -export type IsLiteral = - IsPrimitive extends true - ? IsNotFalse> - : false; +export type IsLiteral = ( + Extends extends true + ? IsNotFalse> + : false +); diff --git a/test-d/is-literal.ts b/test-d/is-literal.ts index 9fb176033..39f404218 100644 --- a/test-d/is-literal.ts +++ b/test-d/is-literal.ts @@ -1,4 +1,5 @@ import {expectType} from 'tsd'; +import type tag from 'tagged-tag'; import type { IsLiteral, IsStringLiteral, @@ -8,33 +9,30 @@ import type { Tagged, LiteralUnion, } from '../index.d.ts'; +import type {Numeric} from '../source/numeric.d.ts'; -const stringLiteral = ''; -const numberLiteral = 1; -// Note: tsd warns on direct literal usage so we cast to the literal type -const bigintLiteral = BigInt(1) as 1n; -const booleanLiteral = true; -const symbolLiteral = Symbol(''); +type stringLiteral = 'aA'; +type numberLiteral = 1; +type bigintLiteral = 1n; +type booleanLiteral = true; +type symbolLiteral = typeof tag; +type numericLiteral = numberLiteral | bigintLiteral; -declare const _string: string; -declare const _number: number; -declare const _bigint: bigint; -declare const _boolean: boolean; -declare const _symbol: symbol; +declare const boolean: boolean; // Literals should be true -expectType>(true); -expectType>(true); -expectType>(true); -expectType>(true); -expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); // Primitives should be false -expectType>(false); -expectType>(false); -expectType>(false); -expectType>(false); -expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); // Null, undefined, and non-primitives should fail all literal checks expectType>(false); @@ -42,10 +40,10 @@ expectType>(false); expectType>(false); expectType>(false); -expectType>(true); -expectType>(false); +expectType>(true); +expectType>(false); -// Strings with infinite set of possible values return `false` +// Strings with infinite set of possible values return `false` in Strict expectType>>(false); expectType>>(false); expectType>>(false); @@ -63,7 +61,25 @@ expectType>>(false); expectType | Uppercase>>(false); expectType>(false); -// Strings with finite set of possible values return `true` +// Strings with infinite set of possible values return `true` in NonStrict +expectType, {strict: false}>>(true); +expectType, {strict: false}>>(true); +expectType, {strict: false}>>(true); +expectType, {strict: false}>>(true); +expectType>, {strict: false}>>(true); +expectType>, {strict: false}>>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType}`, {strict: false}>>(true); +expectType}abc`, {strict: false}>>(true); +expectType>(true); +expectType>(true); +expectType, {strict: false}>>(true); +expectType | Uppercase, {strict: false}>>(true); +expectType>(true); + +// Strings with finite set of possible values return `true` in Strict expectType>(true); expectType>>(true); expectType>>(true); @@ -75,12 +91,32 @@ expectType>(true); expectType | 'C' | 'D'>>(true); expectType | Capitalize<'abc'>>>(true); -// Strings with union of literals and non-literals return `boolean` -expectType | 'abc'>>({} as boolean); -expectType | 'Abc'>>({} as boolean); -expectType>({} as boolean); -expectType>({} as boolean); -expectType>({} as boolean); +// Strings with finite set of possible values return `true` in NonStrict +expectType>(true); +expectType, {strict: false}>>(true); +expectType, {strict: false}>>(true); +expectType, {strict: false}>>(true); +expectType, {strict: false}>>(true); +expectType, {strict: false}>>(true); +expectType, {strict: false}>>(true); +expectType>(true); +expectType | 'C' | 'D', {strict: false}>>(true); +expectType | Capitalize<'abc'>, {strict: false}>>(true); + +// Union of literals and non-literals return `false` +expectType | stringLiteral>>(false); +expectType | stringLiteral>>(false); + +// Union of diffrent literal types return `false` +expectType>(false); +expectType>(false); + +// Strings with union of literals and non-literals return `false` +expectType | 'abc'>>(false); +expectType | 'Abc'>>(false); +expectType>(false); +expectType>(false); +expectType>(false); // Types other than string return `false` expectType>(false); @@ -93,16 +129,19 @@ expectType>(false); expectType>(false); expectType>(false); -expectType>(true); -expectType>(true); -expectType>(false); -expectType>(false); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(false); +expectType>(false); +expectType>(false); -expectType>(true); -expectType>(false); +expectType>(true); +expectType>(false); +expectType>(false); -expectType>(true); -expectType>(false); +expectType>(true); +expectType>(false); // Missing generic parameter // @ts-expect-error @@ -118,25 +157,39 @@ type A4 = IsSymbolLiteral; // Tagged types expectType>>(false); +expectType>>(false); +expectType>>(false); +expectType>>(false); +expectType>>(false); +expectType, {strict: false}>>(false); + expectType, 'Tag'>>>(false); expectType>>(false); expectType>>(true); -expectType>>({} as boolean); -expectType>>({} as boolean); +expectType>>(false); +expectType>>(false); + +expectType, 'Tag'>, {strict: false}>>(true); +expectType, {strict: false}>>(false); +expectType, {strict: false}>>(true); +expectType, {strict: false}>>(true); +expectType, {strict: false}>>(false); expectType | Tagged>>(false); expectType | Tagged<'bar', 'Tag'>>>(true); -expectType | Tagged>>({} as boolean); -expectType | number>>({} as boolean); +expectType | Tagged>>(false); +expectType | number>>(false); + +expectType | Tagged, {strict: false}>>(false); +expectType | Tagged<'bar', 'Tag'>, {strict: false}>>(true); +expectType | Tagged, {strict: false}>>(false); +expectType | number, {strict: false}>>(false); // Uncollapsed unions (e.g., `'foo' | 'bar' | (string & {})`) expectType>(false); expectType>>(false); expectType>>(false); -expectType>>({} as boolean); -expectType>>({} as boolean); +expectType>>(false); +expectType>>(false); expectType, 'Tag'>>>(false); -expectType, 'Tag'>>>({} as boolean); - -expectType>>(false); -expectType>>(false); +expectType, 'Tag'>>>(false); From 650d78b857cb2313d965b3af606f80f620759e90 Mon Sep 17 00:00:00 2001 From: benzaria Date: Tue, 27 May 2025 19:45:37 +0100 Subject: [PATCH 2/9] Fix: `IsStringLiteral` return `false` on unions --- source/is-literal.d.ts | 16 +++++++--------- test-d/is-literal.ts | 23 +++++++++-------------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/source/is-literal.d.ts b/source/is-literal.d.ts index e903fc76e..4852766ee 100644 --- a/source/is-literal.d.ts +++ b/source/is-literal.d.ts @@ -135,22 +135,20 @@ type L2 = Length<`${number}`>; export type IsStringLiteral = ( ApplyDefaultOptions extends infer ResolvedOptions extends Required ? IsNever extends false - ? CollapseLiterals ? UnwrapTagged : T> extends infer Type - ? ResolvedOptions['strict'] extends true - ? IsTrue<_IsStringLiteral> - : LiteralCheck - : never + ? _IsStringLiteral ? UnwrapTagged : T>, ResolvedOptions> : false : never ); -type _IsStringLiteral = ( +type _IsStringLiteral> = ( // If `T` is an infinite string type (e.g., `on${string}`), `Record` produces an index signature, // and since `{}` extends index signatures, the result becomes `false`. S extends string - ? {} extends Record - ? false - : true + ? Options['strict'] extends false + ? LiteralChecks + : {} extends Record + ? false + : true : false ); diff --git a/test-d/is-literal.ts b/test-d/is-literal.ts index 39f404218..6f7016396 100644 --- a/test-d/is-literal.ts +++ b/test-d/is-literal.ts @@ -103,20 +103,15 @@ expectType>(true); expectType | 'C' | 'D', {strict: false}>>(true); expectType | Capitalize<'abc'>, {strict: false}>>(true); -// Union of literals and non-literals return `false` -expectType | stringLiteral>>(false); -expectType | stringLiteral>>(false); - -// Union of diffrent literal types return `false` -expectType>(false); -expectType>(false); - -// Strings with union of literals and non-literals return `false` -expectType | 'abc'>>(false); -expectType | 'Abc'>>(false); -expectType>(false); -expectType>(false); -expectType>(false); +// Union of literals and non-literals return `boolean` +expectType | stringLiteral>>(boolean); +expectType | stringLiteral>>(boolean); +expectType>(boolean); +expectType | 'abc'>>(boolean); +expectType | 'Abc'>>(boolean); +expectType>(boolean); +expectType>(boolean); +expectType>(boolean); // Types other than string return `false` expectType>(false); From 0887bf60c3b4154e8122abb32a0f2e4d8fcfc214 Mon Sep 17 00:00:00 2001 From: benzaria Date: Tue, 27 May 2025 19:56:57 +0100 Subject: [PATCH 3/9] Add: strict option docs --- source/is-literal.d.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/source/is-literal.d.ts b/source/is-literal.d.ts index 4852766ee..d53d6452c 100644 --- a/source/is-literal.d.ts +++ b/source/is-literal.d.ts @@ -11,6 +11,11 @@ import type {And} from './and.d.ts'; @see {@link IsLiteral} */ type IsLiteralOptions = { + /** + Whether to match only finite literal types. + + @default true + */ strict?: boolean; }; @@ -78,6 +83,8 @@ type LiteralChecks = ( /** Returns a boolean for whether the given type is a `string` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). +By default, it's Strict only matching finite literals, See {@link IsLiteralOptions.strict strict} option to change this behaviour. + Useful for: - providing strongly-typed string manipulation functions - constraining strings to be a string literal @@ -294,6 +301,8 @@ type IsLiteralUnion = /** Returns a boolean for whether the given type is a [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). +By default, it's Strict only matching finite literals, See {@link IsLiteralOptions.strict strict} option to change this behaviour. + Useful for: - providing strongly-typed functions when given literal arguments - type utilities, such as when constructing parsers and ASTs From dc1bd3e3f1a322ab048d361dfcc1ae7c47ee5a4d Mon Sep 17 00:00:00 2001 From: benzaria Date: Tue, 27 May 2025 20:04:45 +0100 Subject: [PATCH 4/9] Fix: test file --- test-d/is-literal.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test-d/is-literal.ts b/test-d/is-literal.ts index 6f7016396..e0695219a 100644 --- a/test-d/is-literal.ts +++ b/test-d/is-literal.ts @@ -161,30 +161,30 @@ expectType, {strict: false}>>(false); expectType, 'Tag'>>>(false); expectType>>(false); expectType>>(true); -expectType>>(false); -expectType>>(false); +expectType>>(boolean); +expectType>>(boolean); expectType, 'Tag'>, {strict: false}>>(true); expectType, {strict: false}>>(false); expectType, {strict: false}>>(true); expectType, {strict: false}>>(true); -expectType, {strict: false}>>(false); +expectType, {strict: false}>>(boolean); expectType | Tagged>>(false); expectType | Tagged<'bar', 'Tag'>>>(true); -expectType | Tagged>>(false); -expectType | number>>(false); +expectType | Tagged>>(boolean); +expectType | number>>(boolean); expectType | Tagged, {strict: false}>>(false); expectType | Tagged<'bar', 'Tag'>, {strict: false}>>(true); -expectType | Tagged, {strict: false}>>(false); -expectType | number, {strict: false}>>(false); +expectType | Tagged, {strict: false}>>(boolean); +expectType | number, {strict: false}>>(boolean); // Uncollapsed unions (e.g., `'foo' | 'bar' | (string & {})`) expectType>(false); expectType>>(false); expectType>>(false); -expectType>>(false); -expectType>>(false); +expectType>>(boolean); +expectType>>(boolean); expectType, 'Tag'>>>(false); -expectType, 'Tag'>>>(false); +expectType, 'Tag'>>>(boolean); From 0d87da262ad6792eb49d7559627a517e4767edb5 Mon Sep 17 00:00:00 2001 From: benzaria Date: Sun, 1 Jun 2025 15:11:51 +0100 Subject: [PATCH 5/9] Fix: indentation with the new `xo` config --- source/is-literal.d.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/source/is-literal.d.ts b/source/is-literal.d.ts index d53d6452c..c2c604855 100644 --- a/source/is-literal.d.ts +++ b/source/is-literal.d.ts @@ -44,9 +44,8 @@ type LiteralCheck = ( IsNever extends false // Must be wider than `never` ? [T] extends [LiteralType & infer U] // Remove any branding ? And< - Extends, // Must be narrower than `LiteralType` - Not> // Cannot be wider than `LiteralType` - > + Extends, // Must be narrower than `LiteralType` + Not>> // Cannot be wider than `LiteralType` : false : false ); @@ -74,9 +73,9 @@ type LiteralChecks = ( // If `T` is none of the literal types in the union `LiteralUnionType`, then `LiteralCheck` will evaluate to `false` for the whole union. // If `T` is one of the literal types in the union, it will evaluate to `boolean` (i.e. `true | false`) IsNotFalse< - LiteralUnionType extends Primitive - ? LiteralCheck - : never + LiteralUnionType extends Primitive + ? LiteralCheck + : never > ); @@ -207,11 +206,11 @@ endsWith('abc123', end); @category Utilities */ export type IsNumericLiteral = IsTrue< -T extends number - ? T extends bigint - ? LiteralCheck - : LiteralCheck - : LiteralCheck + T extends number + ? T extends bigint + ? LiteralCheck + : LiteralCheck + : LiteralCheck >; /** From 16424a5f3287d30617f16586cb35b5aafca94109 Mon Sep 17 00:00:00 2001 From: benzaria Date: Fri, 6 Jun 2025 14:09:08 +0100 Subject: [PATCH 6/9] reverte changes on type.d.ts --- source/internal/type.d.ts | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/source/internal/type.d.ts b/source/internal/type.d.ts index 07aae1e81..85dac83ff 100644 --- a/source/internal/type.d.ts +++ b/source/internal/type.d.ts @@ -1,8 +1,7 @@ -import type {Primitive} from '../primitive.d.ts'; -import type {IsNever} from '../is-never.d.ts'; -import type {IsAny} from '../is-any.d.ts'; -import type {And} from '../and.d.ts'; import type {If} from '../if.d.ts'; +import type {IsAny} from '../is-any.d.ts'; +import type {IsNever} from '../is-never.d.ts'; +import type {Primitive} from '../primitive.d.ts'; /** Matches any primitive, `void`, `Date`, or `RegExp` value. @@ -14,13 +13,6 @@ Matches non-recursive types. */ export type NonRecursiveType = BuiltIns | Function | (new (...arguments_: any[]) => unknown); -/** - * Checks if one type extends another. Note: this is not quite the same as `Left extends Right` because: - * 1. If either type is `never`, the result is `true` iff the other type is also `never`. - * 2. Types are wrapped in a 1-tuple so that union types are not distributed - instead we consider `string | number` to _not_ extend `number`. If we used `Left extends Right` directly you would get `Extends` => `false | true` => `boolean`. - */ -export type Extends = IsNever extends true ? IsNever : [Left] extends [Right] ? true : false; - /** Returns a boolean for whether the two given types extends the base type. */ @@ -48,19 +40,9 @@ export type HasMultipleCallSignatures unknow : false; /** -Returns a boolean for whether the given `boolean` Union containe's `false`. -*/ -export type IsNotFalse = Not>; - -/** -Returns a boolean for whether the given `boolean` Union members are all `true`. -*/ -export type IsTrue = Extends; - -/** -Returns a boolean for whether the given `boolean` Union members are all `false`. +Returns a boolean for whether the given `boolean` is not `false`. */ -export type IsFalse = Extends; +export type IsNotFalse = [T] extends [false] ? false : true; /** Returns a boolean for whether the given type is primitive value or primitive type. @@ -77,7 +59,7 @@ IsPrimitive //=> false ``` */ -export type IsPrimitive = Extends; +export type IsPrimitive = [T] extends [Primitive] ? true : false; /** Returns a boolean for whether A is false. From 1234b40ed57ab7c6d765a741ac2f37069686397c Mon Sep 17 00:00:00 2001 From: benzaria Date: Fri, 6 Jun 2025 15:00:21 +0100 Subject: [PATCH 7/9] Refactor type checks in is-literal.d.ts to use ExtendsStrict --- source/is-literal.d.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/source/is-literal.d.ts b/source/is-literal.d.ts index c2c604855..1ecb1bbdd 100644 --- a/source/is-literal.d.ts +++ b/source/is-literal.d.ts @@ -1,6 +1,7 @@ import type {ApplyDefaultOptions, CollapseLiterals} from './internal/object.d.ts'; -import type {Extends, IsNotFalse, IsTrue, Not} from './internal/type.d.ts'; import type {TagContainer, UnwrapTagged} from './tagged.d.ts'; +import type {IsNotFalse, Not} from './internal/type.d.ts'; +import type {ExtendsStrict} from './extends-strict.d.ts'; import type {Primitive} from './primitive.d.ts'; import type {IsNever} from './is-never.d.ts'; import type {Numeric} from './numeric.d.ts'; @@ -44,8 +45,8 @@ type LiteralCheck = ( IsNever extends false // Must be wider than `never` ? [T] extends [LiteralType & infer U] // Remove any branding ? And< - Extends, // Must be narrower than `LiteralType` - Not>> // Cannot be wider than `LiteralType` + ExtendsStrict, // Must be narrower than `LiteralType` + Not>> // Cannot be wider than `LiteralType` : false : false ); @@ -139,11 +140,12 @@ type L2 = Length<`${number}`>; @category Utilities */ export type IsStringLiteral = ( - ApplyDefaultOptions extends infer ResolvedOptions extends Required - ? IsNever extends false - ? _IsStringLiteral ? UnwrapTagged : T>, ResolvedOptions> - : false - : never + IsNever extends false + ? _IsStringLiteral< + CollapseLiterals ? UnwrapTagged : T>, + ApplyDefaultOptions + > + : false ); type _IsStringLiteral> = ( @@ -205,13 +207,12 @@ endsWith('abc123', end); @category Type Guard @category Utilities */ -export type IsNumericLiteral = IsTrue< +export type IsNumericLiteral = T extends number ? T extends bigint ? LiteralCheck : LiteralCheck - : LiteralCheck ->; + : LiteralCheck; /** Returns a boolean for whether the given type is a `true` or `false` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). @@ -338,7 +339,7 @@ stripLeading(str, 'abc'); @category Utilities */ export type IsLiteral = ( - Extends extends true + ExtendsStrict extends true ? IsNotFalse> : false ); From 03a3634dbf2cb5397b4ee126e0bc641f05050fb0 Mon Sep 17 00:00:00 2001 From: benzaria Date: Fri, 20 Jun 2025 14:00:44 +0100 Subject: [PATCH 8/9] feat: revert `IsNumericLiteral` changes, Split `IsStringLiteralOptions` and add better doc --- source/is-literal.d.ts | 91 ++++++++++++++++++++++++++---------------- test-d/is-literal.ts | 88 +++++++++++++++++++++------------------- 2 files changed, 102 insertions(+), 77 deletions(-) diff --git a/source/is-literal.d.ts b/source/is-literal.d.ts index 1ecb1bbdd..3f9f029ab 100644 --- a/source/is-literal.d.ts +++ b/source/is-literal.d.ts @@ -11,17 +11,49 @@ import type {And} from './and.d.ts'; /** @see {@link IsLiteral} */ -type IsLiteralOptions = { +type IsLiteralOptions = + & IsStringLiteralOptions; +// & Is*LiteralOptions... + +/** +@see {@link IsStringLiteral} +*/ +type IsStringLiteralOptions = { /** - Whether to match only finite literal types. + Controls whether wide string patterns such as template literals (`on${string}`), + or utility type results like `Uppercase`, should be treated as **non-literals**. + + When `strictStringCheck` is `true`, types like `on${string}` or `Uppercase` + are considered **not** string literals. + + When set to `false`, those cases are considered **valid** literals. + + @example + ``` + type E1 = IsStringLiteral<`abc${string}`>; + //=> false + + type E2 = IsStringLiteral<`abc${string}`, {strictStringCheck: false}>; + //=> true + + type E3 = IsLiteral>; + //=> false + + type E4 = IsLiteral, {strictStringCheck: false}>; + //=> true + ``` @default true */ - strict?: boolean; + strictStringCheck?: boolean; }; -type DefaultIsLiteralOptions = { - strict: true; +type DefaultIsLiteralOptions = + & DefaultIsStringLiteralOptions; +// & DefaultIs*LiteralOptions... + +type DefaultIsStringLiteralOptions = { + strictStringCheck: true; }; /** @@ -31,23 +63,21 @@ Returns a boolean for whether the given type `T` is the specified `LiteralType`. @example ``` -LiteralCheck<1, number> +type T1 = LiteralCheck<1, number> //=> true -LiteralCheck +type T2 = LiteralCheck //=> false -LiteralCheck<1, string> +type T3 = LiteralCheck<1, string> //=> false ``` */ type LiteralCheck = ( - IsNever extends false // Must be wider than `never` - ? [T] extends [LiteralType & infer U] // Remove any branding - ? And< - ExtendsStrict, // Must be narrower than `LiteralType` - Not>> // Cannot be wider than `LiteralType` - : false + [T] extends [LiteralType & infer U] // Remove any branding + ? And< + ExtendsStrict, // Must be narrower than `LiteralType` + Not>> // Cannot be wider than `LiteralType` : false ); @@ -56,18 +86,16 @@ Returns a boolean for whether the given type `T` is one of the specified literal @example ``` -LiteralChecks<1, Numeric> +type T1 = LiteralChecks<1, Numeric> //=> true -LiteralChecks<1n, Numeric> +type T2 = LiteralChecks<1n, Numeric> //=> true -LiteralChecks +type T3 = LiteralChecks //=> false ``` -@deprecated - */ type LiteralChecks = ( // Conditional type to force union distribution. @@ -83,7 +111,7 @@ type LiteralChecks = ( /** Returns a boolean for whether the given type is a `string` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). -By default, it's Strict only matching finite literals, See {@link IsLiteralOptions.strict strict} option to change this behaviour. +By default, it returns `false` for wide string types like (`on${string}`, `Uppercase`, ...). See {@link IsStringLiteralOptions `IsStringLiteralOptions`}. Useful for: - providing strongly-typed string manipulation functions @@ -139,24 +167,22 @@ type L2 = Length<`${number}`>; @category Type Guard @category Utilities */ -export type IsStringLiteral = ( +export type IsStringLiteral = ( IsNever extends false ? _IsStringLiteral< CollapseLiterals ? UnwrapTagged : T>, - ApplyDefaultOptions + ApplyDefaultOptions > : false ); -type _IsStringLiteral> = ( +type _IsStringLiteral> = ( // If `T` is an infinite string type (e.g., `on${string}`), `Record` produces an index signature, // and since `{}` extends index signatures, the result becomes `false`. S extends string - ? Options['strict'] extends false - ? LiteralChecks - : {} extends Record - ? false - : true + ? Options['strictStringCheck'] extends true + ? {} extends Record ? false : true + : LiteralCheck : false ); @@ -207,12 +233,7 @@ endsWith('abc123', end); @category Type Guard @category Utilities */ -export type IsNumericLiteral = - T extends number - ? T extends bigint - ? LiteralCheck - : LiteralCheck - : LiteralCheck; +export type IsNumericLiteral = LiteralChecks; /** Returns a boolean for whether the given type is a `true` or `false` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). @@ -301,7 +322,7 @@ type IsLiteralUnion = /** Returns a boolean for whether the given type is a [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). -By default, it's Strict only matching finite literals, See {@link IsLiteralOptions.strict strict} option to change this behaviour. +See {@link IsLiteralOptions `IsLiteralOptions`}. Useful for: - providing strongly-typed functions when given literal arguments diff --git a/test-d/is-literal.ts b/test-d/is-literal.ts index e0695219a..2d6398651 100644 --- a/test-d/is-literal.ts +++ b/test-d/is-literal.ts @@ -43,7 +43,8 @@ expectType>(false); expectType>(true); expectType>(false); -// Strings with infinite set of possible values return `false` in Strict +// Strings with infinite set of possible values return `false` in strict +expectType>>(false); expectType>>(false); expectType>>(false); expectType>>(false); @@ -61,25 +62,27 @@ expectType>>(false); expectType | Uppercase>>(false); expectType>(false); -// Strings with infinite set of possible values return `true` in NonStrict -expectType, {strict: false}>>(true); -expectType, {strict: false}>>(true); -expectType, {strict: false}>>(true); -expectType, {strict: false}>>(true); -expectType>, {strict: false}>>(true); -expectType>, {strict: false}>>(true); -expectType>(true); -expectType>(true); -expectType>(true); -expectType}`, {strict: false}>>(true); -expectType}abc`, {strict: false}>>(true); -expectType>(true); -expectType>(true); -expectType, {strict: false}>>(true); -expectType | Uppercase, {strict: false}>>(true); -expectType>(true); - -// Strings with finite set of possible values return `true` in Strict +// Strings with infinite set of possible values return `true` in non-strict +expectType, {strictStringCheck: false}>>(true); +expectType, {strictStringCheck: false}>>(true); +expectType, {strictStringCheck: false}>>(true); +expectType, {strictStringCheck: false}>>(true); +expectType, {strictStringCheck: false}>>(true); +expectType>, {strictStringCheck: false}>>(true); +expectType>, {strictStringCheck: false}>>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType}`, {strictStringCheck: false}>>(true); +expectType}abc`, {strictStringCheck: false}>>(true); +expectType>(true); +expectType>(true); +expectType, {strictStringCheck: false}>>(true); +expectType | Uppercase, {strictStringCheck: false}>>(true); +expectType>(true); + +// Strings with finite set of possible values return `true` in strict +expectType>(true); expectType>(true); expectType>>(true); expectType>>(true); @@ -91,17 +94,18 @@ expectType>(true); expectType | 'C' | 'D'>>(true); expectType | Capitalize<'abc'>>>(true); -// Strings with finite set of possible values return `true` in NonStrict -expectType>(true); -expectType, {strict: false}>>(true); -expectType, {strict: false}>>(true); -expectType, {strict: false}>>(true); -expectType, {strict: false}>>(true); -expectType, {strict: false}>>(true); -expectType, {strict: false}>>(true); -expectType>(true); -expectType | 'C' | 'D', {strict: false}>>(true); -expectType | Capitalize<'abc'>, {strict: false}>>(true); +// Strings with finite set of possible values return `true` in non-strict +expectType>(true); +expectType>(true); +expectType, {strictStringCheck: false}>>(true); +expectType, {strictStringCheck: false}>>(true); +expectType, {strictStringCheck: false}>>(true); +expectType, {strictStringCheck: false}>>(true); +expectType, {strictStringCheck: false}>>(true); +expectType, {strictStringCheck: false}>>(true); +expectType>(true); +expectType | 'C' | 'D', {strictStringCheck: false}>>(true); +expectType | Capitalize<'abc'>, {strictStringCheck: false}>>(true); // Union of literals and non-literals return `boolean` expectType | stringLiteral>>(boolean); @@ -124,7 +128,7 @@ expectType>(false); expectType>(false); expectType>(false); -expectType>(true); +expectType>(false); // ! Needs fixing expectType>(true); expectType>(true); expectType>(false); @@ -156,7 +160,7 @@ expectType>>(false); expectType>>(false); expectType>>(false); expectType>>(false); -expectType, {strict: false}>>(false); +expectType, {strictStringCheck: false}>>(false); expectType, 'Tag'>>>(false); expectType>>(false); @@ -164,21 +168,21 @@ expectType>>(true); expectType>>(boolean); expectType>>(boolean); -expectType, 'Tag'>, {strict: false}>>(true); -expectType, {strict: false}>>(false); -expectType, {strict: false}>>(true); -expectType, {strict: false}>>(true); -expectType, {strict: false}>>(boolean); +expectType, 'Tag'>, {strictStringCheck: false}>>(true); +expectType, {strictStringCheck: false}>>(false); +expectType, {strictStringCheck: false}>>(true); +expectType, {strictStringCheck: false}>>(true); +expectType, {strictStringCheck: false}>>(boolean); expectType | Tagged>>(false); expectType | Tagged<'bar', 'Tag'>>>(true); expectType | Tagged>>(boolean); expectType | number>>(boolean); -expectType | Tagged, {strict: false}>>(false); -expectType | Tagged<'bar', 'Tag'>, {strict: false}>>(true); -expectType | Tagged, {strict: false}>>(boolean); -expectType | number, {strict: false}>>(boolean); +expectType | Tagged, {strictStringCheck: false}>>(false); +expectType | Tagged<'bar', 'Tag'>, {strictStringCheck: false}>>(true); +expectType | Tagged, {strictStringCheck: false}>>(boolean); +expectType | number, {strictStringCheck: false}>>(boolean); // Uncollapsed unions (e.g., `'foo' | 'bar' | (string & {})`) expectType>(false); From 934efc4424c579c85a0979de08ea0dac779165b9 Mon Sep 17 00:00:00 2001 From: benzaria Date: Fri, 20 Jun 2025 14:18:16 +0100 Subject: [PATCH 9/9] refactor: minor changes on JsDoc --- source/is-literal.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/is-literal.d.ts b/source/is-literal.d.ts index 84f21bd2a..444b4596a 100644 --- a/source/is-literal.d.ts +++ b/source/is-literal.d.ts @@ -5,10 +5,11 @@ import type {ExtendsStrict} from './extends-strict.d.ts'; import type {Primitive} from './primitive.d.ts'; import type {IsNever} from './is-never.d.ts'; import type {Numeric} from './numeric.d.ts'; -import type {IsAny} from './is-any.d.ts'; import type {And} from './and.d.ts'; /** +Supports all {@link IsStringLiteralOptions} options. + @see {@link IsLiteral} */ type IsLiteralOptions =