【问题标题】:In Typescript, can I create a mapped type that forces properties to be required?在 Typescript 中,我可以创建强制要求属性的映射类型吗?
【发布时间】:2018-03-19 06:51:04
【问题描述】:

在 Typescript 中,我们已经映射类型,可以将属性从一种类型映射到另一种类型。

一个常见的例子是 Partial 类型,它使泛型类型的所有属性都成为“可选”。

我想实现相反的效果,例如Required,其中所有属性都是“必需的”,而不管它们的可选性如何。

这样的事情可能吗?

(由于了解到我的问题很肤浅,请查看更新!)

更新:

一些上下文...

我正在学习映射类型,并受到文档 here 的启发

type Proxy<T> = {
    get(): T;
    set(value: T): void;
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>;
}
function proxify<T>(o: T): Proxify<T> {
   // ... wrap proxies ...
}
let proxyProps = proxify(props);

我心想,嗯,“我想知道我们能搞什么坏事?”

所以我写了类似的东西:

type ProxyObservable<T> = Observable<T> & {
  [P in keyof T]: ProxyObservable<T[P]>;
};

这很酷,因为你可以给它一个这样的类型:

  interface Foo {
    myNum: number;
    myString: string;
    myObject: {
      someValue: number;
      anotherValue: Date;
    };
  }

你会得到一个ProxyObservable&lt;Foo&gt; 类型,其中每个属性都是同一类型的 Observable,但也有匹配的子属性导航属性(这也是 Observable)。

const foo: ProxyObservable<Foo> = Proxify<Foo>(...)
foo.myNum.subscribe(...)
foo.myObject.subscribe(...)
foo.myObject.someValue.subscribe(...)

不圣洁,我知道。但是很酷。

当您将属性(例如 myObject)设为可选时,就会出现有趣的问题。然后 Typescript 会中断:

foo.myObject.someValue.subscribe(...)

因为它无法辨别要与 Observable 联合的类型是“未定义”还是我们定义的对象。这就是我的问题所在。

但是...我现在也看到这不是“可选”属性的问题,而是一般的可区分联合类型的问题。

我意识到,给定正确的上下文,Typescript 可能能够推断出foo.myObject 属性的类型。我尝试了一些变体:

foo.myObject!.someValue //<- Nope
foo.myObject ? foo.myObject.someValue : ... //<- Nope
if(foo.myObject) foo.myObject.someValue //<- Nope

在我看来,Typescript 编译器应该能够推断出这一点!

谁能告诉我为什么不呢?

【问题讨论】:

    标签: typescript mapped-types


    【解决方案1】:

    这现在是可能的,并且作为 TypeScript 2.8 中的 Required 类型内置到 TypeScript 中。

    http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html

    这是它的定义:

    /**
     * Make all properties in T required
     */
    type Required<T> = {
        [P in keyof T]-?: T[P];
    };
    

    【讨论】:

    • 这不会将子/嵌套属性从可选转换为必需。如何做到这一点?
    【解决方案2】:

    正如@MeirionHughes 所说,类型运算符目前还不够强大,无法做到这一点。

    你为什么要这个?您可以做的一件事是使用inference from mapped types 是创建一个接受Partial&lt;T&gt; 并返回T 的函数。这实际上在类型级别上有效,但实际上实现这样的函数基本上是不可能的,因为它需要能够在运行时获取,比如说,清空对象,并且神奇地知道要哪些属性添加到它。做不到。但无论如何,这是声明:

    declare function unpartial<T>(t: Partial<T>): T;
    

    假设您有一个 concrete 类型,其中包含一些可选属性和一些必需属性:

    interface Stuff {
      foo?: string;
      bar?: number;
      baz: boolean;
      qux: () => void; 
    }
    

    如果您有 unpartial() 函数和 Stuff 的实例,您可以调用它并获取所需类型的对象:

    declare const stuff: Stuff;
    const unpartialStuff = unpartial(stuff);
    type UnpartialStuff = typeof unpartialStuff;
    // UnpartialStuff = { 
    //  foo: string; 
    //  bar: number; 
    //  baz: boolean; 
    //  qux: () => void;
    // }
    

    这很好,但在运行时并不是很好,因为 unpartial 只是声明了但没有实现。这样做你也可以吃蛋糕和吃它:

    const unpartialStuff = (true as false) || unpartial(null! as Stuff);
    type UnpartialStuff = typeof unpartialStuff;
    

    (true as false) 在运行时计算为 true,但被类型系统解释为 false,这意味着在运行时 unpartial() 永远不会被调用,但编译器认为它会为你计算类型。既然我们无论如何都在对编译器撒谎,我们最好不要在Stuff 的假实例上浪费一个变量名;因此(null! as Stuff)

    让我们将所有内容放在一个地方:

    declare function unpartial<T>(t: Partial<T>): T;
    interface Stuff {
      foo?: string;
      bar?: number;
      baz: boolean;
      qux: () => void; 
    }
    const unpartialStuff = (true as false) || unpartial(null! as Stuff);
    type UnpartialStuff = typeof unpartialStuff;
    

    你去吧,你撒谎欺骗了编译器从Stuff计算UnpartialStuff。在你的情况下,这个诡计是否值得,这取决于你。祝你好运!


    更新

    谁能告诉我为什么不呢?

    好吧,它不起作用的原因是keyof (A | B) = (keyof A) &amp; (keyof B),所以keyof (A | undefined) = (keyof A) &amp; never = never。因此,一旦您尝试映射可能是-undefined 类型的键,您就会发现没有键。

    这个身份,其中联合的键是每个组成部分的键的交集,是“安全”身份,因为您知道联合中存在的唯一键是存在于每一个成分中。那是working as intended

    为了映射联合的键,您需要一些不安全的操作,或条件映射类型或其他类型运算符,以便您可以映射每个组成部分。不幸的是,TypeScript 中目前不存在这些类型运算符。 ?

    我不确定如何最好地进行。没有更强大的类型操作的递归映射类型确实是有限的,你已经达到了这些限制之一。除了“等待未来的 TypeScript 功能”之外,也许有人有更好的建议。再次祝你好运!

    【讨论】:

    • 这既美丽又邪恶,感谢您花时间写下来。出于学术兴趣,我更新了我的帖子,为我的问题提供了更好的背景!如果你能看一看,我会很高兴的!
    • 我发布了更新,但我没有任何解决方案,只是对正在发生的事情进行解释。
    【解决方案3】:

    不幸的是,我不认为你可以用类型映射来做一般性的事情。 (很高兴被证明是错误的,如果是这样,我将删除此答案)。 this very question on github too 有一个问题,但一些建议的解决方案对我不起作用。

    一般来说,我认为你需要类型减法,which is still being discussed。您将使用减法从联合类型T | undefined 中“删除”undefined

    我知道“强制”某些属性存在的唯一方法,在某些部分的背后,是使用具有明确已知属性的联合:

    interface Foo {
      a: number;
    }
    
    let a: Partial<Foo> & { a: number } = {}
    

    类型 '{}' 不能分配给类型 '{ a: number; }'。 类型“{}”中缺少属性“a”。

    一个例子:

    class Person {
      id: string;
      name: string = "John";
      address: string = "empty";
    
      constructor(init: Partial<Person> & { id: string }) {
        Object.assign(this, init);
      }
    }   
    
    let person = new Person({ id: "1234" });
    

    你强制 id 在那里,但其他一切都是部分的。

    【讨论】:

    • 感谢类型减法的链接,我很高兴看到它在路线图上。多么有趣的功能!另外,我认为这是一种非常有趣的技术来覆盖某些属性,不幸的是我事先并不知道具体的属性定义。我还发布了一些额外的上下文和对我的问题的更新,如果您有时间看一看,我会非常高兴。
    猜你喜欢
    • 1970-01-01
    • 2018-11-24
    • 2023-04-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-10
    • 1970-01-01
    • 2013-04-03
    相关资源
    最近更新 更多