Typescript: Enforce a type to be "string literal" and not <string>
Asked Answered
G

5

18

Problem

Is there a way in Typescript to define a type that is only a string literal, excluding string itself?

Note that I am not talking about a certain list of string literal; for which, a simple union of "Value1" | "Value2", or an enum type would work. I am talking about any string literal, but not string itself.

Example Code

type OnlyStringLiterals = ...; // <--- what should we put here?

const v1: OnlyStringLiterals = "hi"; // should work
const v2: OnlyStringLiterals = "bye"; // should work
// and so should be for any single string value assigned

// But:
const v3: OnlyStringLiterals = ("red" as string); // should NOT work -- it's string

Use Case

I am doing Branding on the types in my code, and I am passing a brand name, as a template, to my parent class. See the code below:

abstract class MyAbstractClass<
    BRAND_T extends string,
    VALUE_T = string
> {
    constructor(private readonly _value: VALUE_T) { }

    getValue(): VALUE_T { return this._value; }

    private _Brand?: BRAND_T; // required to error on the last line, as intended!
}

class FirstName extends MyAbstractClass<"FirstName"> {
}

class AdminRole extends MyAbstractClass<"AdminRole"> {
}

class SubClassWithMissedName extends MyAbstractClass<string> {
   // I want this to error! ........................ ^^^^^^
}

function printName(name: FirstName) {
    console.log(name.getValue()); 
}

const userFirstName = new FirstName("Alex");
const userRole = new AdminRole("Moderator");

printName(userRole); // Already errors, as expected

Playground Link

I want to make sure every subclass is passing exactly a string literal, and not just string to the parent class.

Glaswegian answered 12/2, 2020 at 9:30 Comment(2)
Your use case does not really explain why you need it. As long as the value is the same - why do you care how it was produced?Antiphonary
@zerkms, I had simplified the use case. Just updated the use case to reflect something closer to my real-world problem. I am concerned about someone missing to pass it. I know one can still claim removing the default type of the second generic type (VALUE_T = string) can enforce two types to be passed; and I can counter-argue that well, since in 95% of the cases the value is string, I rather have a default type for it. :)Glaswegian
G
16

I found an answer that works for my use case, but is not the most reusable one. Just sharing it anyway.

Thought Process

I believe it's not possible to have one solid type to represent what I wanted, because I cannot even think what will show up in VS Code if I hover over it!

However, to my knowledge, there is a function-style checking in Typescript for types that you can pass a type in and expect a type back, and finally assign a value to it to see if it goes through.

Type-checking using a Generic Type and a follow-up assignment

Using this technique I am thinking about the following template type:

type TrueStringLiterals<T extends string> = string extends T ? never : true;

const v1 = "hi";
const check1: TrueStringLiterals<typeof v1> = true; // No error :-)

const v2 = "bye";
const check2: TrueStringLiterals<typeof v2> = true; // No error :-)

const v3 = ("red" as string);
const check3: TrueStringLiterals<typeof v3> = true; // Errors, as expected!

Playground Link

Easier in an already-passed Generic Type

Also, in my use case, I am doing:

abstract class MyAbstractClass<
    BRAND_T extends (string extends BRAND_T ? never : string),
    VALUE_T = string
> {
...

Playground Link

... which works like a charm!

Glaswegian answered 12/2, 2020 at 9:43 Comment(1)
class FirstName extends MyAbstractClass<"FirstName" | "dooo"> {} is not flagged by the compiler. The reason is the 'distributution of conditional types', I think.Josuejosy
P
6

I'd like to submit an answer from a similar question I recently asked, that is far more simple than the examples given so far:

type SpecificString<S extends Exclude<string, S>> = S

let test1: SpecificString<"a" | "b" | "c"> // okay
let test2: SpecificString<string> // error

//guaranteed to work where `Exclude<string, T>` wouldn't
let test3: Exclude<SpecificString<"a" | "1">, "1">
test3 = "a" // okay
test3 = "1" // error

Basically how this works:

Exclude<string, "any string literal"> ==> resolves to string

Exclude<string, string> ==> resolves to never

You can call this F-bounded quantification if you like I guess.

Paripinnate answered 23/7, 2022 at 18:19 Comment(2)
This appears to be the way to go, thanks for sharing!Intercessory
You are providing a utility type (takes <S>). I was looking for a type. See the examples in the problem description.Glaswegian
O
5

You can create utility type which will allow only on subset of string:

type SubString<T> = T extends string ?
    string extends T ? never
    : T
    : never

const makeSubStr = <T extends string>(a: SubString<T>) => a
const a = makeSubStr('strLiteral')
const b = makeSubStr('strLiteral' as string) // error

const c: string = 'elo I am string'
const d = makeSubStr(c) // error

const e: SubString<"red"> = ("red" as string); // error

This type will also return never if something is not a string, in your answer TrueStringLiterals will not take this case into consideration and pass it through.

Oddfellow answered 12/2, 2020 at 9:59 Comment(1)
Thanks. This is very close to my own answer. I believe the <T extends string> in my code is catching what you pointed out. Similarly, I believe you could have made your SubString a one-liner like type SubString<T extends string> = string extends T ? never : T;. The difference is on whether you want x: SubString<number> = 2; to error on SubString<number> type cannot be created in the first place (my way), or to go ahead and errors on assignment to never (your way).Glaswegian
J
1

The other answers don't catch the case where the provided type parameter is a union of literal strings. If this shall be explicitly avoided, as could be read from the OPs question, the following solution, based on the other two can be used:

type UnUnion<T, S> = T extends S ? ([S] extends [T] ? T : never) : never;
type NotUnion<T> = UnUnion<T, T>;
type LiteralString<T extends string> = string extends T ? never : NotUnion<T>;

where UnUnion uses the fact that if T is a union, say 'a' | 'b', the union is distributed over the rest of the type expression.

(['a'|'b'] extends ['a'] ? ... ) | (['a'|'b'] extends ['b'] ? ...)

If T is a union, none of these can hold and all the parts turn into never. NotUnion reduces this to have just one generic parameter and LiteralString just uses its result in case its parameter is not extendable by string.

Playground Link

Josuejosy answered 16/1, 2022 at 14:47 Comment(1)
This still fails for template literal types which are infinite unions, but not string itself, e.g. `a${string}` (at least when the intention is to only allow "any one specific string").Sexpot
T
0

This is a very similar answer to @Aidin's, but is hopefully helpful as it show how to declare types for function parameters.

/**
 * Constraint that ensures that a type is a string literal
 */
type LiteralString<T extends string> = string extends T ? never : T;


function test<T extends string>(s: LiteralString<T>) {}

test('green'); // No error
test('red' as string); // Errors, as expected

Playground link

Towboat answered 15/6, 2024 at 4:12 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.