TypeScript - 仅提取接口成员 - 可能吗?

fre*_*evd 12 typescript

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

let subset = { ...someObject as ISpecific }; 
Run Code Online (Sandbox Code Playgroud)

目前我得到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 }
Run Code Online (Sandbox Code Playgroud)

TypeScript强制转换只是编译器的提示,而不是运行时的实际转换.

Tit*_*mir 13

由于在运行时不存在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); 
Run Code Online (Sandbox Code Playgroud)

  • 但是,不幸的是,由于我们不得不再次声明所有成员,所以它(尽管看起来更加复杂)与(等同于)仅使用完整对象的成员形成一个新的部分对象是相同的,例如:let subset = { someObject.A,someObject.B}; 太糟糕了,如果不明确声明它们,就无法确定接口成员以提取它们。 (2认同)

Fen*_*ton 11

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

let subset = someObject as ISpecific; 
Run Code Online (Sandbox Code Playgroud)

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

interface ISpecific {
    name: string;
}

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

let subset = someObject as ISpecific; 

console.log(subset.age);
Run Code Online (Sandbox Code Playgroud)

确实可以通过像这样的解构来放弃属性,危险在于您需要在之前的列表中包含“所有我不想要的东西” ...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));
Run Code Online (Sandbox Code Playgroud)


Noa*_*Gal 5

我遇到的另一个简单的选择是使用 lodash 的pick功能。这有点乏味,但工作做得很好。

\n

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

\n
class Specific {\n  constructor(readonly a?: string, readonly b?: string) {}\n}\ninterface ISpecific extends Specific {}\ninterface IExtended extends ISpecific {\n  c: string;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
const extended: IExtended = { a: \'type\', b: \'script\', c: \'is cool\' };\n
Run Code Online (Sandbox Code Playgroud)\n

有趣的来了。Specific根据类的新实例以及pick原始对象中的成员获取键列表。
\n换句话说:

\n
const specificMembers: string[] = Object.keys(new Specific());\nconst specific: ISpecific = lodash.pick(extended, specificMembers);\nconsole.log(specific); // {a: "type", b: "script"}\n
Run Code Online (Sandbox Code Playgroud)\n

瞧\xc3\xa0!:)

\n


fre*_*evd 0

可以使用装饰器来实现(见最后的要求)。 它只能与方法一起使用(复制属性 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)
Run Code Online (Sandbox Code Playgroud)

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

  • 我想指出的是,这不是一个好主意,因为您只能提取一个您可以接受的接口。但最大的问题是接口和装饰器之间没有关系。您知道必须将装饰器应用于接口字段,但没有强制执行。如果接口发生变化,您将不知道需要更改类,直到错误报告出现为止。另一种解决方案可确保在存在未匹配的情况下出现编译时错误。 (2认同)