Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add IsXLiteral types #563

Merged
merged 24 commits into from Mar 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
84dd90f
feat: add `IsXLiteral` types
tommy-mitchell Mar 6, 2023
6769a2f
fix: ordering
tommy-mitchell Mar 6, 2023
e4ecfed
chore: remove `IsNull` and `IsUndefined`
tommy-mitchell Mar 6, 2023
79c94a9
fix: missing cases
tommy-mitchell Mar 6, 2023
9a325cb
fix: remove unused type helper
tommy-mitchell Mar 6, 2023
921b11d
feat(`internal`): add `IsNotFalse` type
tommy-mitchell Mar 7, 2023
3537f3b
feat: simplify literal checks
tommy-mitchell Mar 7, 2023
e593486
fix: formatting
tommy-mitchell Mar 7, 2023
4c92b2f
feat: simplify `LiteralCheck`
tommy-mitchell Mar 7, 2023
ecb3d13
feat: simplify `IsNumericLiteral`
tommy-mitchell Mar 7, 2023
da3c48e
feat: scaffold doc comments
tommy-mitchell Mar 7, 2023
13c308f
feat: `IsStringLiteral` documentation
tommy-mitchell Mar 7, 2023
04e690a
fix: add source of example
tommy-mitchell Mar 7, 2023
ef12d12
chore: scaffold more doc comments
tommy-mitchell Mar 7, 2023
e302d94
fix(`test-d`): reorder test cases
tommy-mitchell Mar 7, 2023
d9f3208
feat(`IsLiteral`): add example
tommy-mitchell Mar 8, 2023
bbdcbb4
docs(`IsStringLiteral`): use cases
tommy-mitchell Mar 8, 2023
a6cc701
docs: unify use cases
tommy-mitchell Mar 8, 2023
1af5f31
docs: document type helpers
tommy-mitchell Mar 8, 2023
a5b806a
docs(`IsNumericLiteral`): add example
tommy-mitchell Mar 8, 2023
0b75c16
fix(`IsNumericLiteral`): use cases
tommy-mitchell Mar 8, 2023
5f7ca08
feat(`IsBooleanLiteral`): add example
tommy-mitchell Mar 8, 2023
df6f026
docs(`IsSymbolLiteral`): add example
tommy-mitchell Mar 9, 2023
bded982
chore: add `Type Guard` category
tommy-mitchell Mar 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions index.d.ts
Expand Up @@ -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';
Expand Down
5 changes: 5 additions & 0 deletions readme.md
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions source/internal.d.ts
Expand Up @@ -257,3 +257,8 @@ export type HasMultipleCallSignatures<T extends (...arguments: any[]) => unknown
? false
: true
: false;

/**
Returns a boolean for whether the given `boolean` is not `false`.
*/
export type IsNotFalse<T extends boolean> = [T] extends [false] ? false : true;
252 changes: 252 additions & 0 deletions 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`.
sindresorhus marked this conversation as resolved.
Show resolved Hide resolved

@link https://stackoverflow.com/a/52806744/10292952

@example
```
LiteralCheck<1, number>
//=> true

LiteralCheck<number, number>
//=> false

LiteralCheck<1, string>
//=> false
```
*/
type LiteralCheck<T, LiteralType extends Primitive> = (
IsNever<T> 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
);
tommy-mitchell marked this conversation as resolved.
Show resolved Hide resolved

/**
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<bigint, Numeric>
//=> false
```
*/
type LiteralChecks<T, LiteralUnionType> = (
// Conditional type to force union distribution.
// If `T` is none of the literal types in the union `LiteralUnionType`, then `LiteralCheck<T, LiteralType>` 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<T, LiteralUnionType>
: 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<T extends string> = IsStringLiteral<T> extends true ? Capitalize<T> : string;

// https://github.com/yankeeinlondon/native-dash/blob/master/src/capitalize.ts
function capitalize<T extends Readonly<string>>(input: T): CapitalizedString<T> {
return (input.slice(0, 1).toUpperCase() + input.slice(1)) as CapitalizedString<T>;
}

const output = capitalize('hello, world!');
//=> 'Hello, world!'
```

@category Utilities
@category Type Guard
*/
export type IsStringLiteral<T> = LiteralCheck<T, string>;

/**
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, TEndsWith extends string> =
TValue extends string
? IsStringLiteral<TEndsWith> extends true
? IsStringLiteral<TValue> extends true
? TValue extends `${string}${TEndsWith}`
? true
: false
: boolean
: boolean
: TValue extends number
? IsNumericLiteral<TValue> extends true
? EndsWith<`${TValue}`, TEndsWith>
: false
: false;

function endsWith<Input extends string | number, End extends string>(input: Input, end: End) {
return `${input}`.endsWith(end) as EndsWith<Input, End>;
}

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<T> = LiteralChecks<T, Numeric>;

/**
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<AsString extends boolean> =
IsBooleanLiteral<AsString> extends true
? AsString extends true
? `${typeof id}`
: typeof id
: number | string;

function getId<AsString extends boolean = false>(options?: {asString: AsString}) {
return (options?.asString ? `${id}` : id) as GetId<AsString>;
}

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<T> = LiteralCheck<T, boolean>;

/**
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<Obj extends Record<symbol, number>, Key extends keyof Obj> =
IsSymbolLiteral<Key> extends true
? Obj[Key]
: number;

function get<Obj extends Record<symbol, number>, Key extends keyof Obj>(o: Obj, key: Key) {
return o[key] as Get<Obj, Key>;
}

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<T> = LiteralCheck<T, symbol>;

/** Helper type for `IsLiteral`. */
type IsLiteralUnion<T> =
| IsStringLiteral<T>
| IsNumericLiteral<T>
| IsBooleanLiteral<T>
| IsSymbolLiteral<T>;

/**
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, B> =
A extends string
? B extends string
? IsLiteral<A> extends true
? string extends B ? never : A extends `${B & string}${infer After}` ? After : A
: string
: A
: A;

function stripLeading<Input extends string, Strip extends string>(input: Input, strip: Strip) {
return input.replace(`^${strip}`, '') as StripLeading<Input, Strip>;
}

stripLeading('abc123', 'abc');
//=> '123'

const str = 'abc123' as string;

stripLeading(str, 'abc');
//=> string
```

@category Utilities
@category Type Guard
*/
export type IsLiteral<T extends Primitive> = IsNotFalse<IsLiteralUnion<T>>;
10 changes: 9 additions & 1 deletion 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<IsWhitespace<''>>(false);
expectType<IsWhitespace<' '>>(true);
Expand Down Expand Up @@ -27,3 +27,11 @@ expectType<IsNumeric<' 1.2'>>(false);
expectType<IsNumeric<'1 2'>>(false);
expectType<IsNumeric<'1_200'>>(false);
expectType<IsNumeric<' 1 '>>(false);

expectType<IsNotFalse<true>>(true);
expectType<IsNotFalse<boolean>>(true);
expectType<IsNotFalse<true | false>>(true);
expectType<IsNotFalse<true | false | false | false>>(true);
expectType<IsNotFalse<false>>(false);
expectType<IsNotFalse<false | false>>(false);
expectType<IsNotFalse<false | false | false | false>>(false);
64 changes: 64 additions & 0 deletions 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<IsLiteral<typeof stringLiteral>>(true);
expectType<IsLiteral<typeof numberLiteral>>(true);
expectType<IsLiteral<typeof bigintLiteral>>(true);
expectType<IsLiteral<typeof booleanLiteral>>(true);
expectType<IsLiteral<typeof symbolLiteral>>(true);

// Primitives should be false
expectType<IsLiteral<typeof _string>>(false);
expectType<IsLiteral<typeof _number>>(false);
expectType<IsLiteral<typeof _bigint>>(false);
expectType<IsLiteral<typeof _boolean>>(false);
expectType<IsLiteral<typeof _symbol>>(false);

// Null, undefined, and non-primitives should fail all literal checks
expectType<IsLiteral<null>>(false);
expectType<IsLiteral<undefined>>(false);
expectType<IsLiteral<any>>(false);
expectType<IsLiteral<never>>(false);

expectType<IsStringLiteral<typeof stringLiteral>>(true);
expectType<IsStringLiteral<typeof _string>>(false);

expectType<IsNumericLiteral<typeof numberLiteral>>(true);
expectType<IsNumericLiteral<typeof bigintLiteral>>(true);
expectType<IsNumericLiteral<typeof _number>>(false);
expectType<IsNumericLiteral<typeof _bigint>>(false);

expectType<IsBooleanLiteral<typeof booleanLiteral>>(true);
expectType<IsBooleanLiteral<typeof _boolean>>(false);

expectType<IsSymbolLiteral<typeof symbolLiteral>>(true);
expectType<IsSymbolLiteral<typeof _symbol>>(false);

declare const anything: any;

// Missing generic parameter
expectError<IsLiteral>(anything);
expectError<IsStringLiteral>(anything);
expectError<IsNumericLiteral>(anything);
expectError<IsBooleanLiteral>(anything);
expectError<IsSymbolLiteral>(anything);