【问题标题】:Define TypeScript type that accepts object but not string定义接受对象但不接受字符串的 TypeScript 类型
【发布时间】:2021-03-12 11:30:54
【问题描述】:

短版:

我有一个无法修改的外部类型ExternalFoo。 我需要构建类型Foo,它相当于ExternalFoo,除了它不应该接受string。

type ExternalFoo = {} | number | string;
type Foo = ???;
const foo1: Foo = {}; // ok
const foo1: Foo = 5; // ok
const foo2: Foo = "bar"; // not ok 

是否可以根据给定的ExternalFoo定义Foo来满足这个条件?

加长版 我想构建一个可以接受任何给定T 并返回与T 相同但不允许字符串的T1 的参数化类型。

我有一个 React 应用程序,我想对其进行强制翻译,我们有一个方法 forceTranslation 接受任何 React 元素的方法,并更改指定的属性,使其不再接受 string 但可以接受 @987654333 @。

因此,如果您有一个组件 <User firstName={"bob"}/>,则包装版本 (const TranslatedUser = forceTranslation(User, ["bob"]);) 如果您给它"bob",它将编译时失败,但如果您给它translate("bob"),它将成功。

问题是children 类型的React.ReactNode 属性在其定义中包含{}。使用 Exclude<T, {}> 也会从类型中排除 number(除其他外)。

【问题讨论】:

    标签: typescript


    【解决方案1】:

    所谓的“空对象类型”{}很广泛;几乎所有的值都可以分配给它。粗略地说,它将接受任何可以索引到 like 对象中的值。这包括所有非原始类型,如object,但也包括five primitive types with wrapper objects:所以string、number、boolean、bigint 和symbol 可分配给{},因为值当您像访问对象一样访问它们上的成员时,其中的一些类型会自动包装在 String、Number、Boolean、BigInt 和 Symbol 对象中。

    从某种意义上说,这意味着您无法更改的 ExternalFoo 类型是多余的:{} | string | number 基本上等同于 {}(但两者的显示方式不同,并且在某些情况下编译器可能会以不同方式处理)。我想知道ExternalFoo 的实际意图是否应该更像object | string | number 而不是{} | string | number。例如,它真的是要接受boolean吗?

    const wasThisIntended: ExternalFoo = true;
    

    但你说你不能改变它,所以这超出了范围。


    TypeScript 没有否定类型(请参阅 microsoft/TypeScript#29317),因此您不能使用 ExternalFoo 并快速生成拒绝 string 的版本。像 ExternalFoo & not string 这样的类型在 TypeScript 中根本无法表示,因为它目前存在。 Exclude utility type 仅过滤联合。虽然{} | number | string 可以被过滤以产生{} | number,但{} 本身不能被过滤,因为它不是联合体。

    为了从ExternalFoo 中排除string,我们不仅需要从定义中删除string,还需要将{} 更改为也排除string 的内容。 {} 类型或多或少与 object | string | number | boolean | bigint | symbol 相同。如果需要,我们可以从 that 中删除 string,以生成

    type Foo = object | number | boolean | bigint | symbol;
    

    那么让我们试试吧!

    const foo1: Foo = {}; // okay
    const foo2: Foo = 5; // okay
    const foo3: Foo = "bar"; // error!
    

    看起来不错。根据需要,您可以将{} 和5 分配给Foo 类型的变量,但不能将"bar" 分配给它。这仍然会发生:

    const stillHappens: Foo = true; // okay
    

    但是boolean 不是string,所以我想它也是需要的。


    对于您的较长版本,我建议尝试将泛型类型 T extends ExternalFoo 转换为 T & Foo 并查看它是否适合您:

    type HasLength = { length: number };
    let l: HasLength;
    l = [1, 2, 3]; // okay 
    l = "hello"; // okay
    l = { length: 19 }; // okay
    
    type NotString<T extends ExternalFoo> = T & Foo;
    
    type HasLengthNotString = NotString<{ length: number }>;
    let n: HasLengthNotString;
    n = [1, 2, 3]; // okay
    n = "hello"; // error
    n = { length: 19 }; // okay
      
    

    Playground link to code

    【讨论】:

    • 我正在使用 React。在这种情况下,ExternalFoo 是 ReactNode,其中 includes {} 到 ReactFragment
    • 我也尝试过明确处理 {} 排除,即执行以下操作:object extends ExternalFoo ? Exclude&lt;ExternalFoo, {} |string&gt; | object | TranslatedString,但这也不起作用。
    【解决方案2】:

    您可以使用Exclude 实用程序。

    type ExternalFoo = number | string;
    type Foo = Exclude<ExternalFoo, string>;
    
    const a: Foo = "hi"; // Error: Type 'string' is not assignable to type 'number'.ts(2322)
    

    TypeScript 中的所有内置实用程序: https://www.typescriptlang.org/docs/handbook/utility-types.html

    【讨论】:

    • 但是您将 ExternalFoo 从 {} | number | string 修改为 number | string。
    • 是的@jcalz。使用{} 是一种不好的做法,所以我省略了它。正如TypeScript 本身所说:不要使用{} 作为类型。 {} 实际上意味着“任何非空值”。 - 如果你想要一个表示“任何对象”的类型,你可能想要Record&lt;string, unknown&gt;。 - 如果你想要一个表示“任何值”的类型,你可能想要unknown。 - 如果你想要一个表示“空对象”的类型,你可能想要Record&lt;string, never&gt;。你是对的@jcalz。谢谢。
    • 我不确定你是否理解我评论的上下文:问题是“我有一个无法修改的外部类型ExternalFoo”,所以修改ExternalFoo,无论多么好会是这样做,似乎超出了这个问题的范围。我同意{} 可能不是任何人想要的。
    • 正如@jcalz 所指出的。这绕过了我的问题。投反对票以反映这一点。
    • 是的@jcalz。我明白了。我只是试图指出Exclude 的用法。谢谢
    猜你喜欢
    • 2022-11-16
    • 2015-07-02
    • 1970-01-01
    • 1970-01-01
    • 2012-11-19
    • 2011-03-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多