Update:
I've reformulated everything to adhere to jcalz's
clean style:
type IsStringLiteral<T> =
string extends T ? false : // must be narrower than string
[T] extends [never | undefined | null] ? false : // must be wider than never and nullable
[T] extends [string] ? true : // must be wider than string
false;
type IsNumberLiteral<T> =
number extends T ? false : // must be narrower than number
[T] extends [never | undefined | null] ? false : // must be wider than never and nullable
[T] extends [number] ? true : // must be wider than number
false;
type IsSingleTypeLiteral<T> =
IsStringLiteral<T> extends false ?
IsNumberLiteral<T> :
true;
type IsLiteral<T> =
string extends T ? false : // must be narrower than string
number extends T ? false : // must be narrower than number
[T] extends [never | undefined | null] ? false : // must be wider than never and nullable
[T] extends [number | string] ? true : // must be wider than number | string
false;
That was a bit more challenging than expected, but after hours of work I finally accomplished this:
type Switch<A, B, IF, ELSE = A> = A extends B ? IF : ELSE;
type IsStringLiteral<T> =
// Check for nullable type using Switch type. See next comment why Switch must be used.
Switch<T, undefined | null, true, false> extends true ? false : (
// `T extends string` does not work for `"str" | number` and etc. Results in `boolean` type.
// Need to use boolean Switch to filter out false-positive.
Switch<T, string, true, false> extends true ? (
// `string` does not extend literal type.
string extends T ? false : true
) : false
);
type IsNumberLiteral<T> =
Switch<T, undefined | null, true, false> extends true ? false : (
Switch<T, number, true, false> extends true ? (
number extends T ? false : true
) : false
);
type IsSingleTypeLiteral<T> =
Switch<IsStringLiteral<T>, false, IsNumberLiteral<T>, true>;
type IsLiteral<T> =
// `"string literal" | string` and etc. will return a false-positive `boolean` type.
// `boolean` type must always be `false`, thus `false extends boolean` is used to get that `false` type.
Switch<false, Switch<T, undefined | null, true, false> extends true ? false : (
T extends string | number ? (
string extends T ? false : (number extends T ? false : true)
) : false
), false, true>;
Here are some demonstration cases presented in the form of an HTML table (tested with 3.1.1
):
table, th, td {
white-space: nowrap;
border: 1px solid black;
}
<table><tbody><tr><th>Test scenarios</th><th>IsStringLiteral</th><th>IsNumberLiteral</th><th>IsSingleTypeLiteral</th><th>IsLiteral</th></tr><tr><td>"string literal"</td><td><b>true</b></td><td>false</td><td><b>true</b></td><td><b>true</b></td></tr><tr><td>123</td><td>false</td><td><b>true</b></td><td><b>true</b></td><td><b>true</b></td></tr><tr><td>string</td><td>false</td><td>false</td><td>false</td><td>false</td></tr><tr><td>object</td><td>false</td><td>false</td><td>false</td><td>false</td></tr><tr><td>[]</td><td>false</td><td>false</td><td>false</td><td>false</td></tr><tr><td>[string, number]</td><td>false</td><td>false</td><td>false</td><td>false</td></tr><tr><td>any</td><td>false</td><td>false</td><td>false</td><td>false</td></tr><tr><td>void</td><td>false</td><td>false</td><td>false</td><td>false</td></tr><tr><td>null</td><td>false</td><td>false</td><td>false</td><td>false</td></tr><tr><td>undefined</td><td>false</td><td>false</td><td>false</td><td>false</td></tr><tr><td>never</td><td>false</td><td>false</td><td>false</td><td>false</td></tr><tr><td>"string literal" | 123</td><td>false</td><td>false</td><td>false</td><td><b>true</b></td></tr><tr><td>"string literal" | string</td><td>false</td><td>false</td><td>false</td><td>false</td></tr><tr><td>123 | number</td><td>false</td><td>false</td><td>false</td><td>false</td></tr></tbody></table>