diff --git a/index.d.ts b/index.d.ts index 5770e8731..9bd37ed98 100644 --- a/index.d.ts +++ b/index.d.ts @@ -76,6 +76,13 @@ export type {HasRequiredKeys} from './source/has-required-keys'; export type {Spread} from './source/spread'; export type {TupleToUnion} from './source/tuple-to-union'; export type {IsEqual} from './source/is-equal'; +export type { + IsLiteral, + IsStringLiteral, + IsNumericLiteral, + IsBooleanLiteral, + IsSymbolLiteral, +} from './source/is-literal'; // Template literal types export type {CamelCase} from './source/camel-case'; diff --git a/readme.md b/readme.md index 245862986..9fcc5a548 100644 --- a/readme.md +++ b/readme.md @@ -171,6 +171,11 @@ Click the type names for complete docs. - [`HasRequiredKeys`](source/has-required-keys.d.ts) - Create a `true`/`false` type depending on whether the given type has any required fields. - [`Spread`](source/spread.d.ts) - Mimic the type inferred by TypeScript when merging two objects or two arrays/tuples using the spread syntax. - [`IsEqual`](source/is-equal.d.ts) - Returns a boolean for whether the two given types are equal. +- [`IsLiteral`](source/is-literal.d.ts) - Returns a boolean for whether the given type is a [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). +- [`IsStringLiteral`](source/is-literal.d.ts) - 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). +- [`IsNumericLiteral`](source/is-literal.d.ts) - 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). +- [`IsBooleanLiteral`](source/is-literal.d.ts) - 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). +- [`IsSymbolLiteral`](source/is-literal.d.ts) - Returns a boolean for whether the given type is a `symbol` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). ### JSON diff --git a/source/internal.d.ts b/source/internal.d.ts index 46c2d0920..6870d2b1b 100644 --- a/source/internal.d.ts +++ b/source/internal.d.ts @@ -257,3 +257,8 @@ export type HasMultipleCallSignatures unknown ? false : true : false; + +/** +Returns a boolean for whether the given `boolean` is not `false`. +*/ +export type IsNotFalse = [T] extends [false] ? false : true; diff --git a/source/is-literal.d.ts b/source/is-literal.d.ts new file mode 100644 index 000000000..ef458cab1 --- /dev/null +++ b/source/is-literal.d.ts @@ -0,0 +1,252 @@ +import type {Primitive} from './primitive'; +import type {Numeric} from './numeric'; +import type {IsNever, IsNotFalse} from './internal'; + +/** +Returns a boolean for whether the given type `T` is the specified `LiteralType`. + +@link https://stackoverflow.com/a/52806744/10292952 + +@example +``` +LiteralCheck<1, number> +//=> true + +LiteralCheck +//=> false + +LiteralCheck<1, string> +//=> false +``` +*/ +type LiteralCheck = ( + IsNever extends false // Must be wider than `never` + ? [T] extends [LiteralType] // Must be narrower than `LiteralType` + ? [LiteralType] extends [T] // Cannot be wider than `LiteralType` + ? false + : true + : false + : false +); + +/** +Returns a boolean for whether the given type `T` is one of the specified literal types in `LiteralUnionType`. + +@example +``` +LiteralChecks<1, Numeric> +//=> true + +LiteralChecks<1n, Numeric> +//=> true + +LiteralChecks +//=> false +``` +*/ +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 + > +); + +/** +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). + +Useful for: + - providing strongly-typed string manipulation functions + - constraining strings to be a string literal + - type utilities, such as when constructing parsers and ASTs + +@example +``` +import type {IsStringLiteral} from 'type-fest'; + +type CapitalizedString = IsStringLiteral extends true ? Capitalize : string; + +// https://github.com/yankeeinlondon/native-dash/blob/master/src/capitalize.ts +function capitalize>(input: T): CapitalizedString { + return (input.slice(0, 1).toUpperCase() + input.slice(1)) as CapitalizedString; +} + +const output = capitalize('hello, world!'); +//=> 'Hello, world!' +``` + +@category Utilities +@category Type Guard +*/ +export type IsStringLiteral = LiteralCheck; + +/** +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). + +Useful for: + - providing strongly-typed functions when given literal arguments + - type utilities, such as when constructing parsers and ASTs + +@example +``` +import type {IsNumericLiteral} from 'type-fest'; + +// https://github.com/inocan-group/inferred-types/blob/master/src/types/boolean-logic/EndsWith.ts +type EndsWith = + TValue extends string + ? IsStringLiteral extends true + ? IsStringLiteral extends true + ? TValue extends `${string}${TEndsWith}` + ? true + : false + : boolean + : boolean + : TValue extends number + ? IsNumericLiteral extends true + ? EndsWith<`${TValue}`, TEndsWith> + : false + : false; + +function endsWith(input: Input, end: End) { + return `${input}`.endsWith(end) as EndsWith; +} + +endsWith('abc', 'c'); +//=> true + +endsWith(123456, '456'); +//=> true + +const end = '123' as string; + +endsWith('abc123', end); +//=> boolean +``` + +@category Utilities +@category Type Guard +*/ +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). + +Useful for: + - providing strongly-typed functions when given literal arguments + - type utilities, such as when constructing parsers and ASTs + +@example +``` +import type {IsBooleanLiteral} from 'type-fest'; + +const id = 123; + +type GetId = + IsBooleanLiteral extends true + ? AsString extends true + ? `${typeof id}` + : typeof id + : number | string; + +function getId(options?: {asString: AsString}) { + return (options?.asString ? `${id}` : id) as GetId; +} + +const numberId = getId(); +//=> 123 + +const stringId = getId({asString: true}); +//=> '123' + +declare const runtimeBoolean: boolean; +const eitherId = getId({asString: runtimeBoolean}); +//=> number | string +``` + +@category Utilities +@category Type Guard +*/ +export type IsBooleanLiteral = LiteralCheck; + +/** +Returns a boolean for whether the given type is a `symbol` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). + +Useful for: + - providing strongly-typed functions when given literal arguments + - type utilities, such as when constructing parsers and ASTs + +@example +``` +import type {IsSymbolLiteral} from 'type-fest'; + +type Get, Key extends keyof Obj> = + IsSymbolLiteral extends true + ? Obj[Key] + : number; + +function get, Key extends keyof Obj>(o: Obj, key: Key) { + return o[key] as Get; +} + +const symbolLiteral = Symbol('literal'); +const symbolValue: symbol = Symbol('value'); + +get({[symbolLiteral]: 1} as const, symbolLiteral); +//=> 1 + +get({[symbolValue]: 1} as const, symbolValue); +//=> number +``` + +@category Utilities +@category Type Guard +*/ +export type IsSymbolLiteral = LiteralCheck; + +/** Helper type for `IsLiteral`. */ +type IsLiteralUnion = + | IsStringLiteral + | IsNumericLiteral + | IsBooleanLiteral + | IsSymbolLiteral; + +/** +Returns a boolean for whether the given type is a [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). + +Useful for: + - providing strongly-typed functions when given literal arguments + - type utilities, such as when constructing parsers and ASTs + +@example +``` +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; + +function stripLeading(input: Input, strip: Strip) { + return input.replace(`^${strip}`, '') as StripLeading; +} + +stripLeading('abc123', 'abc'); +//=> '123' + +const str = 'abc123' as string; + +stripLeading(str, 'abc'); +//=> string +``` + +@category Utilities +@category Type Guard +*/ +export type IsLiteral = IsNotFalse>; diff --git a/test-d/internal.ts b/test-d/internal.ts index ad7a44705..631966a54 100644 --- a/test-d/internal.ts +++ b/test-d/internal.ts @@ -1,5 +1,5 @@ import {expectType} from 'tsd'; -import type {IsWhitespace, IsNumeric} from '../source/internal'; +import type {IsWhitespace, IsNumeric, IsNotFalse} from '../source/internal'; expectType>(false); expectType>(true); @@ -27,3 +27,11 @@ expectType>(false); expectType>(false); expectType>(false); expectType>(false); + +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(false); +expectType>(false); +expectType>(false); diff --git a/test-d/is-literal.ts b/test-d/is-literal.ts new file mode 100644 index 000000000..0ee48a819 --- /dev/null +++ b/test-d/is-literal.ts @@ -0,0 +1,64 @@ +import {expectError, expectType} from 'tsd'; +import type { + IsLiteral, + IsStringLiteral, + IsNumericLiteral, + IsBooleanLiteral, + IsSymbolLiteral, +} from '../index'; + +const stringLiteral = ''; +const numberLiteral = 1; +// @ts-expect-error: suppress BigInt literal tsd warning +const bigintLiteral = 1n; +const booleanLiteral = true; +const symbolLiteral = Symbol(''); + +declare const _string: string; +declare const _number: number; +declare const _bigint: bigint; +declare const _boolean: boolean; +declare const _symbol: symbol; + +// Literals should be 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); + +// Null, undefined, and non-primitives should fail all literal checks +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); + +expectType>(true); +expectType>(false); + +expectType>(true); +expectType>(true); +expectType>(false); +expectType>(false); + +expectType>(true); +expectType>(false); + +expectType>(true); +expectType>(false); + +declare const anything: any; + +// Missing generic parameter +expectError(anything); +expectError(anything); +expectError(anything); +expectError(anything); +expectError(anything);