【问题标题】:TypeScript - extract interface members only - possible?TypeScript - 仅提取接口成员 - 可能吗?
【发布时间】:2018-11-23 04:12:33
【问题描述】:

有没有办法从属于接口的对象中动态提取成员(即不再明确指定它们),如下所示:

let subset = { ...someObject as ISpecific }; 

目前我得到了 someObject 碰巧拥有的所有成员。 所以扩展运算符在这里不起作用。有没有办法做到这一点?

例子:

interface ISpecific { A: string; B: string; }
class Extended implements ISpecific { public A: string = '1'; public B: string = '2'; public C: string = '3'; }

let someObject = new Extended(); 
let subset = { ...someObject as ISpecific }; 
console.log(subset);  // -> { A, B, C } but want { A, B }

TypeScript 转换只是对编译器的提示,而不是运行时的真正转换。

【问题讨论】:

  • someObject 包含更多字段,您只想提取 ISpecific 中的字段?
  • 是的,我刚刚找到了解决方案,见下文。
  • 由于缺乏反射或语言级别的支持,TypeScript 中还没有解决方案。必须明确指定要包含的成员(或指定要在破坏模式中丢弃的成员)。
  • 目前这在 TypeScript 中是不可能的,而且可能永远不会。 design goals 将以下内容视为非目标5。在程序中添加或依赖运行时类型信息,或根据类型系统的结果发出不同的代码。相反,鼓励不需要运行时元数据的编程模式。 您必须制定一个在运行时有效的解决方案。至少如果为该解决方案使用映射类型,如果输出与您的接口不兼容,编译器仍会报告错误。

标签: typescript


【解决方案1】:

更新:这似乎不起作用,所以不是答案。

let subset = Object.create(someObject as ISpecific); 

Object.create 使用指定的原型创建一个新对象,但 TypeScript 转换只是对编译器的提示。它可能使用具体的类层次结构然后引用原型来工作,但这可能包括比接口声明的更多的实际成员。

【讨论】:

  • 呃 .. 如果这是你想要的,值得商榷 .. 这将保留 someObject 中的所有对象,它不会删除 ISpecific 中不存在的额外属性
  • 是的,我不想修改原来的(someObject)。我只想要其成员的一个子集(理想情况下不在新对象中,但这不可能在 javascript 中,除非我将另一个原型添加到链中)。
  • 也许我不太了解您的要求,但是使用您的代码此let someObject = { a: "", b: 10 }; let subset = Object.create(someObject as ISpecific); console.log(subset.b); 将输出10,该字段仍将存在,您创建了您没有创建的原件的完整副本删除任何字段
  • 我没有很好地解释我试图实现的目标。我将在问题中发布一个示例..
  • 我更新了问题以包含一个示例。看起来我之前的解决方案不是解决方案,因为我无法在一个简单的示例中使其工作。
【解决方案2】:

如果你想限制你使用的类型,你可以简单而安全地做到这一点:

let subset = someObject as ISpecific; 

subset 上的属性仍将存在,但编译器会阻止您依赖它们,即subset.age 将在下面失败,尽管该属性确实仍然存在。

interface ISpecific {
    name: string;
}

const someObject = {
    name: 'Fenton',
    age: 21
};

let subset = someObject as ISpecific; 

console.log(subset.age);

你可以真的通过像这样解构来放弃属性,危险在于你需要在...subset之前的列表中包含“所有我不想要的东西”。

interface ISpecific {
    name: string;
}

const someObject = {
    name: 'Fenton',
    age: 21
};

let { age, ...subset } = someObject;   

console.log(JSON.stringify(someObject));
console.log(JSON.stringify(subset));

【讨论】:

  • 谢谢,但我想发布结果,所以不能访问其他成员(不仅被编译器隐藏)。我的接口是一个供消费者使用的 API,而实现该接口的我的类包含不可访问的私有逻辑。是的,我可以明确地选择成员(或者像你展示的那样放弃其他成员),但我很好奇是否有任何语言功能可以隐式地做到这一点。接口成员的反射似乎也不可能在 TypeScript 中使用。
  • PS:API 使用者是普通的 JavaScript 用户,因此所有 TypeScript 安全性都消失了,如果存在隐藏成员,则可以访问。
  • @frevd - 第二个示例满足了这一需求,尽管您可能希望遵循具有显式 API 响应类的约定以确保没有数据泄漏,并使用许多自动映射器克隆之一来映射,或通过创建显式映射方法。最大的安全漏洞之一是通过 API 意外暴露数据,因此在该领域做到防弹是一件好事
【解决方案3】:

由于 typescript 接口在运行时不存在,我们不能使用它们来指导任何运行时行为,只能在编译时进行类型检查。然而,我们可以创建一个与接口具有相同属性的对象(具有true 类型的所有属性,例如以简化初始化),如果该对象具有比接口更多或更少的字段,则使编译器触发错误。我们可以使用这个对象作为我们提取哪些属性的指南:

function extract<T>(properties: Record<keyof T, true>){
    return function<TActual extends T>(value: TActual){
        let result = {} as T;
        for (const property of Object.keys(properties) as Array<keyof T>) {
            result[property] = value[property];
        }
        return result;
    }
}

interface ISpecific { A: string; B: string; }
const extractISpecific = extract<ISpecific>({ 
    // This object literal is guaranteed by the compiler to have no more and no less properties then ISpecific
    A: true,
    B: true
})
class Extended implements ISpecific { public A: string = '1'; public B: string = '2'; public C: string = '3'; }

let someObject = new Extended(); 
let subset = extractISpecific(someObject); 

【讨论】:

  • 漂亮!这看起来比我迄今为止使用的 factoryfunction+implementor 解决方案要好得多。我必须阅读 Typescript 规范,不知道 keyof 运算符(也不知道 for..of 循环)。
  • 通过使用 TActual 进行柯里化来使用类型推断的有趣方式。也感谢您指出 Record 类型定义(也必须阅读类型定义)。
  • 不幸的是,由于我们必须再次重述所有成员,它(虽然看起来更复杂)就像(相当于)仅使用完整的成员形成一个新的部分对象对象,例如:let subset = { someObject.A, someObject.B };太糟糕了,如果不明确说明它们,就无法确定接口的成员以提取它们。
  • 我刚刚有了一个想法:在每个成员上使用一个装饰器,并让该装饰器在某个隐藏属性中收集成员,这是一组属性,然后使用一个简单的函数从对象中提取(不重述任何内容)。我试试看..
  • 感谢您提供的精美代码! @TitianCernicova-Dragomir
【解决方案4】:

可以使用装饰器来实现(见文末要求)。 它只能与方法一起使用(复制属性 get/set 访问器只会产生它的瞬时返回值,而不是访问器函数)。

// define a decorator (@publish) for marking members of a class for export: 
function publish(targetObj: object, memberKey: string, descriptor: PropertyDescriptor) { 
    if (!targetObj['_publishedMembers']) 
        targetObj['_publishedMembers'] = []; 
    targetObj['_publishedMembers'].push(memberKey); 
}

// this function can return the set of members of an object marked with the @publish decorator: 
function getPublishedMembers(fromObj: object) {
    const res = {}; 
    const members = fromObj['_publishedMembers'] || []; 
    members.forEach(member => { res[member] = fromObj[member].bind(fromObj); }); 
    return res; 
}

// this is for making sure all members are implemented (does not make sure about being marked though): 
interface IPublishedMembers {
    A(): string; 
    B(): number; 
    C(): void; 
}

// this class implements the interface and has more members (that we do NOT want to expose): 
class Full implements IPublishedMembers {
    private b: number = 0xb; 

    @publish public A(): string { return 'a'; }
    @publish public B(): number { return this.b; }
    @publish public C(): boolean { return true; }
    public D(): boolean { return !this.C(); } 
    public E(): void { } 
}

const full = new Full(); 
console.log(full);  // -> all members would be exposed { A(), B(), b, C(), D(), E() }

const published = getPublishedMembers(full) as IPublishedMembers; 
console.log(published);  // -> only sanctioned members { A(), B(), C() }
console.log(published.B());  // -> 11 = 0xb (access to field of original object works)

(这需要 tsconfig.json 中的 compilerOption "experimentalDecorators":true 和一个 ES5 目标,更多信息请参见 http://www.typescriptlang.org/docs/handbook/decorators.html

【讨论】:

  • 我想指出这不是一个好主意,因为你只能提取一个你可以忍受的界面。但是最大的问题是接口和装饰器之间没有任何关系。您知道您必须将装饰器应用于接口字段,但没有强制执行此操作。如果接口发生变化,在错误报告出现之前,您不会知道需要更改类。另一种解决方案可确保在未匹配时出现编译时错误。
  • 是的,它是有限的,但没有添加成员那么痛苦。如果接口发生变化,编译器会强制我实现新成员(因为类本身实现了接口)。它不会强迫我将装饰器应用于新方法,我必须记住把它放在它前面(不是大的)。我的例子只是一个例子,当然你可以让它更复杂,支持多个集合(例如@publish('api1')甚至@publish()),属性访问器等。现在这就是我所需要的(不是通用解决方案),您帮助指出了前进的方向,再次感谢:)
  • 也许 ES7 会提供一些功能来根据其原型之一切出对象的一部分,TypeScript 无法真正强制执行。
【解决方案5】:

很遗憾,您需要指定并重复这些部分。

export interface Machine {
  id: string;
  name: string;
  logo: string;
  location: Location;
  products: Item[];
  restriction: string;
  categories: string[];
  star: number;
  distance: number;
}

export interface MachineIdentification {
  id: string;
  name: string;
  logo: string;
  location: Location;
}

//Object Destructuring fat object in parts
const {id, name, logo, location} = machine; 

//Compose a ligth object from parts and apply interface type
const mi: MachineIdentification = {id, name, logo, location}; 

【讨论】:

    【解决方案6】:

    我遇到的另一个简单选项是使用 lodash 的 pick 函数。这有点乏味,但做得很好。

    首先,定义一个代表您的界面的类。稍后您将需要它来轻松创建该类的对象。

    class Specific {
      constructor(readonly a?: string, readonly b?: string) {}
    }
    interface ISpecific extends Specific {}
    interface IExtended extends ISpecific {
      c: string;
    }
    

    那么假设这是您要从中提取数据的原始对象:

    const extended: IExtended = { a: 'type', b: 'script', c: 'is cool' };
    

    现在是有趣的部分。基于类的新实例化获取Specific 键的列表,以及来自原始对象的pick 这些成员。
    换句话说:

    const specificMembers: string[] = Object.keys(new Specific());
    const specific: ISpecific = lodash.pick(extended, specificMembers);
    console.log(specific); // {a: "type", b: "script"}
    

    瞧! :)

    【讨论】:

      猜你喜欢
      • 2019-04-17
      • 1970-01-01
      • 2017-09-23
      • 1970-01-01
      • 2018-02-28
      • 2020-03-10
      • 1970-01-01
      • 1970-01-01
      • 2020-11-07
      相关资源
      最近更新 更多