Typescript:强制类型为“字符串字面量”,而不是<字符串>。

14

问题

在Typescript中,有没有一种方式可以定义一个类型,它只是字符串字面量,而不包括string本身?

请注意,我并不是在谈论某个特定的字符串字面量列表;对于这种情况,简单的联合类型"Value1" | "Value2"或枚举类型将起作用。我说的是任何字符串字面量,但不包括string本身。

示例代码

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

应用场景

我正在对代码中的类型进行品牌化,并将品牌名称作为模板传递给我的父类。请参见下面的代码:

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

游乐场链接

我希望确保每个子类向父类传递的是一个字符串字面量,而不仅仅是字符串


你的使用情况并没有真正解释为什么你需要它。只要值相同,你为什么在意它是如何产生的呢? - zerkms
@zerkms,我已经简化了用例。刚刚更新了用例以反映更接近于我的实际问题。我担心有人会忘记传递它。我知道有人仍然可以声称删除第二个泛型类型(VALUE_T = string)的默认类型可以强制传递两种类型;而我可以反驳说,在95%的情况下,值都是字符串,我宁愿为其设置一个默认类型。 :) - Aidin
4个回答

13
我发现了一个对我的使用情况有效的答案,但不是最可重用的答案。无论如何,我还是想分享一下。
思路
我认为不可能有一个实心类型来表示我想要的内容,因为我甚至不能想象在VS Code中悬停在它上面会显示什么!
然而,据我所知,在Typescript中有一种函数式检查类型的方法,你可以传递一个类型并期望得到一个类型,最后将一个值赋给它以查看是否通过。
使用通用类型和后续分配进行类型检查
使用这种技术,我考虑以下模板类型:
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!

在已经传递的泛型类型中更加简单

同样,在我的使用情况下,我正在进行以下操作:

演示链接

abstract class MyAbstractClass<
    BRAND_T extends (string extends BRAND_T ? never : string),
    VALUE_T = string
> {
...
游乐场链接

......非常好用!


class FirstName extends MyAbstractClass<"FirstName" | "dooo"> {}没有被编译器标记。我认为原因是“条件类型的分布”。 - Harald

5
您可以创建效用类型,只允许使用字符串的子集:
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

如果某个内容不是字符串类型,这种类型也会返回never。在你的答案中,TrueStringLiterals不会考虑这种情况并将其通过。


1
谢谢。这非常接近我的答案。我相信我代码中的<T extends string>已经捕捉到了你指出的问题。同样,我认为你可以像type SubString<T extends string> = string extends T ? never : T;一样将你的SubString变成一行代码。区别在于你想让x: SubString<number> = 2;在第一次创建SubString<number>类型时就报错(我的方式),还是在分配给never时报错(你的方式)。 - Aidin

1
我想提交一个来自于我最近提出的类似问题的链接,这个答案比目前给出的例子要简单得多:answer
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

基本上,它的工作原理如下:

Exclude<string, "any string literal"> ==> 解析为 string

Exclude<string, string> ==> 解析为 never

如果你愿意,你可以将此称为F-bounded quantification

1
这似乎是正确的方法,感谢分享! - 6infinity8
您正在提供一种实用类型(接受<S>)。我正在寻找一种类型。请参考问题描述中的示例。 - Aidin
您正在提供一种实用类型(接受<S>)。我正在寻找一种类型。请参考问题描述中的示例。 - undefined

1
其他答案没有涵盖提供的类型参数是字符串文字联合的情况。如果要明确避免这种情况,可以从OP的问题中看出,可以使用以下基于其他两个解决方案的解决方案:
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>;

“UnUnion”使用以下事实:如果T是一个联合类型,比如“'a' | 'b'”,那么该联合类型会分布到类型表达式的其余部分中。
(['a'|'b'] extends ['a'] ? ... ) | (['a'|'b'] extends ['b'] ? ...)

如果 T 是一个联合类型,则上述情况都不成立,所有部分都会变成 neverNotUnion 将其简化为只有一个泛型参数,而 LiteralString 则在其参数不可扩展为 string 的情况下使用其结果。

游乐场链接


这仍然无法处理无限联合的模板文字类型,但不包括 string 本身,例如 \a${string}``(至少当意图是只允许“任何一个特定字符串”时)。 - ASDFGerte

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接