FrogTheFrog
FrogTheFrog

Reputation: 1671

Determine if type is a 'string' literal, 'number' literal or 'string | number' literal

Since the typescript now supports conditional types, I've decided to do some meta-programing to add more flavor to VSCODE intellisense. However, while other types are easy to separate using A extends B I have a hard time determining if the provided type is literal.

So the question would be - how do I determine if given type is of literal type?

Upvotes: 4

Views: 477

Answers (2)

FrogTheFrog
FrogTheFrog

Reputation: 1671

Edit: I've rewritten everything to match 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 little harder than anticipated, but after few hours I managed to achieve 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 test cases in form of HTML table (tested with 3.1.1):

table, th, td {
  white-space: nowrap;
  border: 1px solid black;
}
<table><tbody><tr><th>Test cases</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>

Upvotes: 0

jcalz
jcalz

Reputation: 328132

I'm not sure what your use cases are. Personally, I would do something like this:

type IfStringOrNumberLiteral<T, Y=true, N=false> =
  string extends T ? N : // must be narrower than string
  number extends T ? N : // must be narrower than number
  [T] extends [never] ? N : // must be wider than never
  [T] extends [string | number] ? Y : // must be narrower than string | number
  N

I always use --strictNullChecks so your mileage may vary when it comes to how that treats null and undefined. Of course it can be amended to meet any particular need you have. Mostly I just wanted to show an alternative to circuitous constructs of the form ( X extends Y ? true : false ) extends true ? U : V.

Hope that helps; good luck.

Upvotes: 5

Related Questions