如何减少 javascript 对象以仅包含来自界面的属性

IT技术 javascript angularjs typescript
2021-03-20 09:46:39

使用 typescript 时,声明的接口可能如下所示:

interface MyInterface {
  test: string;
}

具有额外属性的实现可能是这样的:

class MyTest implements MyInterface {
  test: string;
  newTest: string;
}

示例(此处变量“reduced”仍包含属性“newTest”):

var test: MyTest = {test: "hello", newTest: "world"}

var reduced: MyInterface = test; // something clever is needed

问题

一般来说,如何使“reduced”变量仅包含在“MyInterface”接口中声明的属性。

为什么

在将 angular.toJson 发送到 rest 服务之前尝试使用 'reduced' 变量时会出现问题 - toJson 方法转换 newTest 变量,即使它在编译期间无法在实例上访问,这使得 rest 服务无法接受 json,因为它具有不应该存在的属性。

6个回答

这是不可能的。原因interface是 Typescript 构造并且转译的 JS 代码是空的

//this code transpiles to empty!
interface MyInterface {
  test: string;
}

因此,在运行时没有什么可“使用”的——不存在可供查询的属性。

@jamesmoey 的回答解释了实现预期结果的解决方法。我使用的类似解决方案只是将“接口”定义为一个类 -

class MyInterface {
  test: string = undefined;
}

然后您可以使用lodash从“接口”中选择属性以注入您的对象:

import _ from 'lodash';  //npm i lodash

const before = { test: "hello", newTest: "world"};
let reduced = new MyInterface();
_.assign(reduced , _.pick(before, _.keys(reduced)));
console.log('reduced', reduced)//contains only 'test' property

JSFiddle

这是一个务实的解决方案,它很好地为我服务,而不会陷入关于它是否实际上是接口和/或命名约定(例如IMyInterfaceMyInterface)的语义上,并允许您模拟和单元测试

谢谢,看起来很有趣。我更新了测试变量,使其与测试属性的名称不同,以便更容易理解。
2021-05-13 09:46:39

TS 2.1 具有 Object Spread 和 Rest,因此现在可以:

var my: MyTest = {test: "hello", newTest: "world"}

var { test, ...reduced } = my;

之后,reduced 将包含除“test”之外的所有属性。

@dwllama 我认为你倒退了。命名属性是丢弃的属性。...reduced是您想要保留的部分(尽管您不知道里面有什么,除了它不包含test)。
2021-04-23 09:46:39
这有效,但它需要您确切知道哪些属性将是“多余的”。如果你不知道呢?
2021-04-27 09:46:39
是的。对象传播允许您分解属性,休息以削减属性。
2021-05-04 09:46:39
@medley56 我不明白是怎么回事。它只需要您知道要保留哪些属性。您按名称分配那些,并丢弃其余的而不爆炸和分配它们。
2021-05-14 09:46:39
你能解释一下,你所说的对象传播和休息是什么意思?你是说三个点...吧?
2021-05-17 09:46:39

另一种可能的方法:

正如其他答案所提到的,您无法避免在运行时执行某些操作;TypeScript 编译为 JavaScript,主要是通过简单地删除接口/类型定义、注释和断言。类型系统被擦除MyInterface在需要它的运行时代码中无处可寻。

因此,您将需要类似要保留在缩减对象中的键数组之类的东西:

const myTestKeys = ["test"] as const;

这本身是脆弱的,因为如果MyInterface被修改,您的代码可能不会注意到。让您的代码注意到的一种可能方法是设置一些类型别名定义,如果myTestKeys与 不匹配,则会导致编译器错误keyof MyInterface

// the following line will error if myTestKeys has entries not in keyof MyInterface:
type ExtraTestKeysWarning<T extends never =
  Exclude<typeof myTestKeys[number], keyof MyInterface>> = void;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type 'UNION_OF_EXTRA_KEY_NAMES_HERE' does not satisfy the constraint 'never'

// the following line will error if myTestKeys is missing entries from keyof MyInterface:
type MissingTestKeysWarning<T extends never =
  Exclude<keyof MyInterface, typeof myTestKeys[number]>> = void;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type 'UNION_OF_MISSING_KEY_NAMES_HERE' does not satisfy the constraint 'never'

这不是很漂亮,但是如果您更改MyInterface,上面的一行或两行将给出一个错误,希望该错误具有足够的表现力,开发人员可以修改myTestKeys

有很多方法可以使这更通用,或者可能不那么侵入性,但几乎无论你做什么,你可以合理地期望 TypeScript 的最好结果是你的代码在面对接口的意外更改时会发出编译器警告;并不是说您的代码在运行时实际上会做不同的事情。


一旦你有了你关心的键,你就可以编写一个pick()函数来从对象中提取这些属性:

function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
  return keys.reduce((o, k) => (o[k] = obj[k], o), {} as Pick<T, K>);
}

我们可以在您的test对象上使用它来获取reduced

var test: MyTest = { test: "hello", newTest: "world" }

const reduced: MyInterface = pick(test, ...myTestKeys);

console.log(JSON.stringify(reduced)); // {"test": "hello"}

这样可行!

Playground 链接到代码

您是否尝试仅设置/分配仅在界面上列出的属性?这样的功能在 TypeScript 中是不可用的,但是编写一个函数来执行您正在寻找的行为非常简单。

interface IPerson {
    name: string;
}

class Person implements IPerson {
	name: string = '';
}
class Staff implements IPerson {
	name: string = '';
    position: string = '';
}

var jimStaff: Staff = {
    name: 'Jim',
    position: 'Programmer'
};

var jim: Person = new Person();
limitedAssign(jimStaff, jim);
console.log(jim);

function limitedAssign<T,S>(source: T, destination: S): void {
    for (var prop in destination) {
        if (source[prop] && destination.hasOwnProperty(prop)) {
            destination[prop] = source[prop];
        }
    }
}

@TomasF 我遇到了同样的问题并想通了。请参阅github.com/Microsoft/TypeScript/issues/6515,因为底层 JS 没有设置默认属性,因此for循环没有要迭代的属性。解决方案是为每个属性添加默认值,例如name: string = '';
2021-04-29 09:46:39
这就像我所追求的那样。我无法让它工作。我相信这是因为 'jim' 上的属性未初始化,并且找不到任何要循环的属性。另外 - 如果有某种方法可以跳过类“Person”并直接从 IPerson 循环遍历属性 - 那将是完美的。
2021-05-07 09:46:39

在您的示例中,将无法通过减少的变量访问newTest属性,因此这就是使用类型的目标。typescript带来了类型检查,但它不操作对象属性。

我们的问题是,在将 angular.toJson 发送到休息服务之前,我们尝试将其与 angular.toJson 一起使用 - 并且 toJson 甚至会转换 newTest 变量,即使它不能从实例访问。
2021-05-16 09:46:39