4

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?

2
  • Do you consider "a" | "b" to be of literal type? What about 2 | string? Commented Oct 14, 2018 at 19:54
  • I consider "a" | "b", 1 | 2 and maybe "a" | 1 to be of literal type. Any other union is invalid for me. Commented Oct 14, 2018 at 20:07

2 Answers 2

5

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.

Sign up to request clarification or add additional context in comments.

2 Comments

This looks so clean! Thank you! Just curious, [...] are for cosmetic purpose only?
It's to prevent distributing the conditional type over T. Since T is what's called a "naked type parameter", the phrase T extends string | number ? Y : N could end up becoming Y | N if T is a union like 4 | object, and I think you just want N in that case. The way to prevent distributive conditional types it is to "clothe" the naked type parameter in some (covariant) way. The tersest way to do that is the one-tuple bracket syntax.
0

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>

3 Comments

"...after few hours I managed to achieve this" You asked the question 4 minutes ago and answered it the same minute?
@Johan When I'm playing Jeopardy! I go all the way :) stackoverflow.blog/2011/07/01/…
Cheers, I was just really confused, didn't know about the format. Now I know I didn't travel in time :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.