ChatGPT解决这个技术问题 Extra ChatGPT

使用 Typescript 进行接口类型检查

这个问题直接类似于 Class type check with TypeScript

我需要在运行时找出任何类型的变量是否实现了接口。这是我的代码:

interface A{
    member:string;
}

var a:any={member:"foobar"};

if(a instanceof A) alert(a.member);

如果在 typescript playground 中输入此代码,最后一行将被标记为错误,“当前范围内不存在名称 A”。但这不是真的,该名称确实存在于当前范围内。我什至可以将变量声明更改为 var a:A={member:"foobar"}; 而无需编辑器抱怨。在浏览网页并在 SO 上找到另一个问题后,我将接口更改为一个类,但是我不能使用对象文字来创建实例。

我想知道 A 类型是如何消失的,但是查看生成的 javascript 可以解释问题:

var a = {
    member: "foobar"
};
if(a instanceof A) {
    alert(a.member);
}

没有将 A 表示为接口,因此无法进行运行时类型检查。

我知道 javascript 作为一种动态语言没有接口的概念。有什么方法可以对接口进行类型检查吗?

typescript playground 的自动补全显示 typescript 甚至提供了一个方法 implements。我该如何使用它?

JavaScript 没有接口的概念,但这并不是因为它是一种动态语言。这是因为接口还没有实现。
是的,但是您可以使用类代替接口。请参阅 this 示例。
显然不是在 2017 年。现在超级相关的问题。
来自 C# 背景,写作当天的所有解决方案都很糟糕!它涉及复制复制和损害代码可读性。

J
John Miller

您可以在没有 instanceof 关键字的情况下实现您想要的,因为您现在可以编写自定义类型保护:

interface A {
    member: string;
}

function instanceOfA(object: any): object is A {
    return 'member' in object;
}

var a: any = {member: "foobar"};

if (instanceOfA(a)) {
    alert(a.member);
}

很多会员

如果您需要检查大量成员以确定对象是否与您的类型匹配,则可以添加一个鉴别器。下面是最基本的示例,需要您管理自己的鉴别器......您需要更深入地了解模式以确保避免重复的鉴别器。

interface A {
    discriminator: 'I-AM-A';
    member: string;
}

function instanceOfA(object: any): object is A {
    return object.discriminator === 'I-AM-A';
}

var a: any = {discriminator: 'I-AM-A', member: "foobar"};

if (instanceOfA(a)) {
    alert(a.member);
}

“没有办法运行时检查接口。”有,他们只是出于某种原因还没有实施它。
如果接口有100个成员,你需要检查所有100个吗?美食吧。
您可以为您的对象添加一个鉴别器,而不是检查所有 100...
此鉴别器范例(如此处所写)不支持扩展接口。如果检查派生接口是否是基接口的 instanceOf,则派生接口将返回 false。
不敢相信我们已经到了 2020 年,而且没有比这更好的方法了……=/
E
Egel

在 TypeScript 1.6 中,user-defined type guard 将完成这项工作。

interface Foo {
    fooProperty: string;
}

interface Bar {
    barProperty: string;
}

function isFoo(object: any): object is Foo {
    return 'fooProperty' in object;
}

let object: Foo | Bar;

if (isFoo(object)) {
    // `object` has type `Foo`.
    object.fooProperty;
} else {
    // `object` has type `Bar`.
    object.barProperty;
}

正如 Joe Yang 所说:从 TypeScript 2.0 开始,您甚至可以利用标记联合类型。

interface Foo {
    type: 'foo';
    fooProperty: string;
}

interface Bar {
    type: 'bar';
    barProperty: number;
}

let object: Foo | Bar;

// You will see errors if `strictNullChecks` is enabled.
if (object.type === 'foo') {
    // object has type `Foo`.
    object.fooProperty;
} else {
    // object has type `Bar`.
    object.barProperty;
}

它也适用于 switch


这看起来相当好奇。显然有某种可用的元信息。为什么要使用这种类型保护语法来公开它。由于哪些约束,函数旁边的“对象是接口”起作用,而不是 isinstanceof ?更准确地说,您可以直接在 if 语句中使用“对象即接口”吗?但无论如何,非常有趣的语法,来自我的 +1。
@lhk 不,没有这样的声明,它更像是一种特殊类型,它告诉如何在条件分支内缩小类型。由于 TypeScript 的“范围”,我相信即使在未来也不会有这样的说法。 object is typeobject instanceof class 之间的另一个不同之处在于,TypeScript 中的类型是结构性的,它只关心“形状”而不是对象从哪里获得形状:普通对象或类的实例,它不关心没关系。
只是为了澄清这个答案可能会产生的误解:在运行时没有元信息来扣除对象类型或其接口。
@mostruash 是的,答案的后半部分即使编译也不会在运行时工作。
哦,但是,这必须假定在运行时这些对象将使用 type 属性创建。在这种情况下,它可以工作。这个例子没有显示这个事实。
J
Joe Yang

typescript 2.0 引入标记联合

Typescript 2.0 features

interface Square {
    kind: "square";
    size: number;
}

interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}

interface Circle {
    kind: "circle";
    radius: number;
}

type Shape = Square | Rectangle | Circle;

function area(s: Shape) {
    // In the following switch statement, the type of s is narrowed in each case clause
    // according to the value of the discriminant property, thus allowing the other properties
    // of that variant to be accessed without a type assertion.
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.width * s.height;
        case "circle": return Math.PI * s.radius * s.radius;
    }
}

我正在使用 2.0 beta 但标记的 union 不起作用。 2.0
使用夜间构建编译,但智能感知不起作用。它还列出了错误:“Square | 类型”上不存在属性宽度/大小/...矩形 |在案例陈述中圈出。但它编译。
这实际上只是使用了鉴别器。
是的!这岩石!如此干净,我喜欢干净和最简单的可能
你必须在创建对象时指定它的种类?这是不可接受的!
C
Caleb Macdonald Black

用户定义的类型守卫怎么样? https://www.typescriptlang.org/docs/handbook/advanced-types.html

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function isFish(pet: Fish | Bird): pet is Fish { //magic happens here
    return (<Fish>pet).swim !== undefined;
}

// Both calls to 'swim' and 'fly' are now okay.

if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}

这是我最喜欢的答案 - 类似于 stackoverflow.com/a/33733258/469777,但没有可能因缩小等原因而中断的魔术字符串。
由于某种原因,这对我不起作用,但 (pet as Fish).swim !== undefined; 起作用了。
当您将 swim(); 添加到 Bird 时会发生什么,因为您有一只宠物鸭?每只宠物都会被识别为鱼,不是吗?
@Kayz 我猜当您使用 isFish 时,您的代码并不真正关心对象是否属于任意鱼类别,您更关心您的对象是否支持游泳操作。也许更好的函数名称可能会反映这一点,例如 isAquatic 或其他东西。这种识别对象类型的方法称为鸭子类型,如果需要,您可以进一步研究。但简而言之,如果鸭子会游泳,那么它就是鱼,我们有一个命名问题要解决。 en.wikipedia.org/wiki/Duck_typing
如果“宠物是鱼”,那么通过鱼或鸟的能力有什么意义?这是可怕的可读性!
P
PEHLAJ

现在有可能,我刚刚发布了一个增强版的 TypeScript 编译器,它提供了完整的反射功能。您可以从其元数据对象实例化类,从类构造函数中检索元数据并在运行时检查接口/类。你可以看看here

使用示例:

在您的一个打字稿文件中,创建一个接口和一个实现它的类,如下所示:

interface MyInterface {
    doSomething(what: string): number;
}

class MyClass implements MyInterface {
    counter = 0;

    doSomething(what: string): number {
        console.log('Doing ' + what);
        return this.counter++;
    }
}

现在让我们打印一些已实现接口的列表。

for (let classInterface of MyClass.getClass().implements) {
    console.log('Implemented interface: ' + classInterface.name)
}

用reflect-ts编译并启动它:

$ node main.js
Implemented interface: MyInterface
Member name: counter - member kind: number
Member name: doSomething - member kind: function

有关 Interface 元类型的详细信息,请参见反射.d.ts。

更新:您可以找到完整的工作示例here


否决了因为我认为这很愚蠢,但随后暂停了一秒钟,查看了您的 github 页面,发现它一直是最新的并且有据可查,因此被赞成:-) 我现在仍然无法证明自己现在使用它只是为了implements 但想承认您的承诺并且不想刻薄 :-)
实际上,我看到这种反射特性的主要目的是创建更好的 IoC 框架,就像 Java 世界长期以来已经拥有的框架(Spring 是第一个也是最重要的框架)。我坚信 TypeScript 可以成为未来最好的开发工具之一,而反射是它真正需要的功能之一。
...呃,那又如何,我们必须将这些编译器“增强”应用到任何未来的 Typescript 版本中?这实际上是 Typescript 的一个分支,而不是 Typescript 本身,对吧?如果是这样,这不是一个可行的长期解决方案。
@dudewad 正如在许多其他主题中所说,这是一个临时解决方案。我们正在等待通过转换器的编译器可扩展性。请参阅官方 TypeScript 存储库中的相关问题。此外,所有被广泛采用的强类型语言都有反射,我认为 TypeScript 也应该有。和我一样,许多其他用户也这么认为。
这正是概念验证的目的:向人们证明事情是可以完成的。问题指出:“我知道 javascript 作为一种动态语言没有接口的概念。有没有办法对接口进行类型检查?”答案是:不,没有修改/改进,但如果我们有办法扩展/改进语言和编译器,是的。问题是:谁来决定改变?但这是另一个话题。
D
DS.

这是另一种选择:模块 ts-interface-builder 提供了一个构建时工具,可将 TypeScript 接口转换为运行时描述符,ts-interface-checker 可以检查对象是否满足它。

对于OP的例子,

interface A {
  member: string;
}

您首先运行 ts-interface-builder,它会生成一个带有描述符的新简明文件,例如 foo-ti.ts,您可以像这样使用它:

import fooDesc from './foo-ti.ts';
import {createCheckers} from "ts-interface-checker";
const {A} = createCheckers(fooDesc);

A.check({member: "hello"});           // OK
A.check({member: 17});                // Fails with ".member is not a string" 

您可以创建一个单行类型保护函数:

function isA(value: any): value is A { return A.test(value); }

A 仅指一种类型,但在这里用作值。 return A.test(value);
您必须与示例中的内容不同。 const {A} = ... 创造了价值 A
ts-interface-checker 对我来说效果很好。
W
Willem van der Veen

Typescript 中的类型保护:

TS 有用于此目的的类型保护。他们以下列方式定义它:

执行运行时检查以保证某个范围内的类型的某些表达式。

这基本上意味着 TS 编译器在有足够信息的情况下可以将类型缩小到更具体的类型。例如:

function foo (arg: number | string) {
    if (typeof arg === 'number') {
        // fine, type number has toFixed method
        arg.toFixed()
    } else {
        // Property 'toFixed' does not exist on type 'string'. Did you mean 'fixed'?
        arg.toFixed()
        // TSC can infer that the type is string because 
        // the possibility of type number is eliminated at the if statement
    }
}

回到您的问题,我们还可以将类型保护的这个概念应用于对象以确定它们的类型。要为对象定义类型保护,我们需要定义一个返回类型为类型谓词的函数。例如:

interface Dog {
    bark: () => void;
}

// The function isDog is a user defined type guard
// the return type: 'pet is Dog' is a type predicate, 
// it determines whether the object is a Dog
function isDog(pet: object): pet is Dog {
  return (pet as Dog).bark !== undefined;
}

const dog: any = {bark: () => {console.log('woof')}};

if (isDog(dog)) {
    // TS now knows that objects within this if statement are always type Dog
    // This is because the type guard isDog narrowed down the type to Dog
    dog.bark();
}

D
Daniel Ribeiro

我想指出,TypeScript 并没有提供直接的机制来动态测试对象是否实现了特定的接口。

相反,TypeScript 代码可以使用 JavaScript 技术来检查对象上是否存在一组适当的成员。例如:

var obj : any = new Foo();

if (obj.someInterfaceMethod) {
    ...
}

如果你有一个复杂的形状怎么办?您不想在每个深度级别对每个属性进行硬编码
@Tom我想您可以(作为检查器函数的第二个参数)传递运行时值或示例/示例-即您想要的接口对象。然后,代替硬编码代码,您编写任何您想要的接口示例......并编写一些一次性对象比较代码(使用例如 for (element in obj) {})来验证两个对象是否具有相似的元素类似的类型。
D
Dan Dohotaru

与上面使用 user-defined guards 的情况相同,但这次使用箭头函数谓词

interface A {
  member:string;
}

const check = (p: any): p is A => p.hasOwnProperty('member');

var foo: any = { member: "foobar" };
if (check(foo))
    alert(foo.member);

D
Dmitry Matveev

类型守卫

interface MyInterfaced {
    x: number
}

function isMyInterfaced(arg: any): arg is MyInterfaced {
    return arg.x !== undefined;
}

if (isMyInterfaced(obj)) {
    (obj as MyInterfaced ).x;
}

“arg is MyInterfaced”是一个有趣的注释。如果失败会怎样?看起来像一个编译时接口检查——这正是我一开始想要的。但是如果编译器检查参数,为什么还要有一个函数体。如果可以进行这样的检查,为什么将其移至单独的功能。
@lhk 刚刚阅读了关于类型保护的打字稿文档... typescriptlang.org/docs/handbook/advanced-types.html
@DmitryMatveev orrr ...只需回答完全合理的问题,而不是指向没有的文档?
@lhk 不确定您是否对此仍有疑问,但无论如何,我会尝试实际上回答它。你是对的,它是一个编译时检查。 arg is MyInterfaced 位告诉编译器:“如果分支调用此函数并且结果为真,则接受对被测试为 MyInterfaced 类型的对象的所有进一步使用”。可能导致您混淆的原因可以用该语句中的关键位突出显示,即“如果结果为真”。不幸的是,这取决于开发人员来确定什么构成 MyInterfaced
我说“不幸的是”,因为为了通常确定任何给定对象是否属于任何给定接口类型,这种方法没有那么无用。
f
frodeborli

我认为这是最好的方法;在接口上附加一个“Fubber”符号。它的编写速度要快得多,对于 JavaScript 引擎来说比类型保护要快得多,支持接口的继承,并且在需要时使类型保护易于编写。

这就是 ES6 使用符号的目的。

界面

// Notice there is no naming conflict, because interfaces are a *type*
export const IAnimal = Symbol("IAnimal"); 
export interface IAnimal {
  [IAnimal]: boolean; // the fubber
}

export const IDog = Symbol("IDog");
export interface IDog extends IAnimal {
  [IDog]: boolean;
}

export const IHound = Symbol("IDog");
export interface IHound extends IDog {
  // The fubber can also be typed as only 'true'; meaning it can't be disabled.
  [IDog]: true;
  [IHound]: boolean;
}

班级

import { IDog, IAnimal } from './interfaces';
class Dog implements IDog {
  // Multiple fubbers to handle inheritance:
  [IAnimal] = true;
  [IDog] = true;
}

class Hound extends Dog implements IHound {
  [IHound] = true;
}

测试

如果您想帮助 TypeScript 编译器,可以将此代码放入类型保护中。

import { IDog, IAnimal } from './interfaces';

let dog = new Dog();

if (dog instanceof Hound || dog[IHound]) {
  // false
}
if (dog[IAnimal]?) {
  // true
}

let houndDog = new Hound();

if (houndDog[IDog]) {
  // true
}

if (dog[IDog]?) {
  // it definitely is a dog
}


我使用 getter get [ISymbol](){return true} 因为它不会为每个实例创建一个属性。
@AlexanderLonberg 是的;这样可行。想知道这将如何优化;在您的情况下,引擎必须首先检查对象,然后转到原型并调用 getter 函数。就我而言,可能有很多重复。在任何一种情况下,javascript 引擎都可以优化成本。
哦,一个little slow
@AlexanderLonberg 谢谢,很高兴有一些实际数字可供参考。 Firefox 和 Chrome 似乎都优化了调用静态 getter 方法的成本。我相信几年前这些数字会大不相同。
a
aledpardo

基于 Fenton 的 answer,这是我实现的一个函数,用于验证给定的 object 是否具有 interface 的全部或部分键。

根据您的用例,您可能还需要检查每个接口属性的类型。下面的代码没有这样做。

function implementsTKeys<T>(obj: any, keys: (keyof T)[]): obj is T {
    if (!obj || !Array.isArray(keys)) {
        return false;
    }

    const implementKeys = keys.reduce((impl, key) => impl && key in obj, true);

    return implementKeys;
}

使用示例:

interface A {
    propOfA: string;
    methodOfA: Function;
}

let objectA: any = { propOfA: '' };

// Check if objectA partially implements A
let implementsA = implementsTKeys<A>(objectA, ['propOfA']);

console.log(implementsA); // true

objectA.methodOfA = () => true;

// Check if objectA fully implements A
implementsA = implementsTKeys<A>(objectA, ['propOfA', 'methodOfA']);

console.log(implementsA); // true

objectA = {};

// Check again if objectA fully implements A
implementsA = implementsTKeys<A>(objectA, ['propOfA', 'methodOfA']);

console.log(implementsA); // false, as objectA now is an empty object

A
Anthony Gingrich

自OP以来接近9年,这个问题仍然存在。我真的很想爱 Typescript。通常我会成功。但它在类型安全方面的漏洞是我捏着的鼻子无法阻挡的恶臭。

我的 goto 解决方案并不完美。但我认为它们比大多数更常用的解决方案要好。鉴别器已被证明是一种不好的做法,因为它们限制了可扩展性并完全违背了类型安全的目的。我的两个最漂亮的对接丑陋的解决方案是,按顺序:

类装饰器:递归扫描类型化对象的成员并根据符号名称计算散列。将散列与静态 KVP 属性中的类型名称相关联。在哈希计算中包含类型名称以降低与祖先的歧义风险(发生在空子类中)。优点:它被证明是最值得信赖的。它还提供了非常严格的执行。这也类似于其他高级语言如何原生实现多态性。然而,该解决方案需要进一步扩展才能真正实现多态性。缺点:匿名/JSON 对象必须在每次类型检查时重新散列,因为它们没有要关联和静态缓存的类型定义。在高负载情况下,过多的堆栈开销会导致严重的性能瓶颈。可以使用 IoC 容器来缓解,但对于没有其他理由的小型应用程序来说,这也可能是不希望的开销。还需要额外的努力才能将装饰器应用于每个需要它的对象。

克隆:非常丑陋,但可以通过深思熟虑的策略受益。创建类型化对象的新实例,并反射性地从匿名对象复制顶级成员分配。给定一个预先确定的通道标准,您可以同时检查和克隆转换为类型。类似于其他语言的“tryParse”。优点:在某些情况下,可以通过立即使用转换后的“测试”实例来减轻资源开销。装饰师不需要额外的勤奋。大量的弹性公差。缺点:内存泄漏就像面粉筛一样。如果没有“深度”克隆,变异的引用可能会破坏其他组件,而没有预料到会破坏封装。静态缓存不适用,因此对每个调用都执行操作——具有大量顶级成员的对象会影响性能。刚接触 Typescript 的开发人员会因为不理解您编写这种模式的原因而将您误认为是初级开发人员。

总而言之:我不为 Typescript 在多态性中的细微差别买“JS 不支持它”的借口。转译器绝对适合这个目的。用盐来治疗伤口:它来自微软。许多年前,他们已经成功地解决了同样的问题:.Net Framework 提供了一个强大的互操作 API,用于向后兼容 COM 和 ActiveX。他们没有尝试转换到较旧的运行时。对于像 JS 这样松散和解释的语言来说,这种解决方案会更容易,也不会那么混乱……但他们害怕失去对其他超集的支持而畏缩不前。使用 JS 中本应由 TS 解决的缺点,作为重新定义静态类型化面向对象原则的格式错误的基础是——好吧——胡说八道。它与数十年来为高级软件开发提供信息的大量行业领先的文档和规范背道而驰。


考虑添加代码示例,更容易阅读带有简短解释的小代码示例。
e
edbentley

您可以使用 ts-validate-type 在运行时验证 TypeScript 类型,如下所示(虽然需要 Babel 插件):

const user = validateType<{ name: string }>(data);

x
xinthose

我在文件 filter-descriptor.interface.d.ts 中找到了来自 @progress/kendo-data-query 的示例

检查器

declare const isCompositeFilterDescriptor: (source: FilterDescriptor | CompositeFilterDescriptor) => source is CompositeFilterDescriptor;

示例用法

const filters: Array<FilterDescriptor | CompositeFilterDescriptor> = filter.filters;

filters.forEach((element: FilterDescriptor | CompositeFilterDescriptor) => {
    if (isCompositeFilterDescriptor(element)) {
        // element type is CompositeFilterDescriptor
    } else {
        // element type is FilterDescriptor
    }
});

B
Björn Hjorth

在 Typescript 中使用 Reflect 进行类型保护

这是我的 Typescript 游戏引擎中的类型保护示例

 export interface Start {
    /**
     * Start is called on the frame when a script is enabled just before any of the Update methods are called the first time.
     */
     start(): void
 }


/**
  * User Defined Type Guard for Start
  */
 export const implementsStart = (arg: any): arg is Start => {
     return Reflect.has(arg, 'start')
 } 


 /**
  * Example usage of the type guard
  */

 start() {
    this.components.forEach(component => {
        if (implementsStart(component)) {
            component.start()
        }  

    })
}

G
Geoff Davids

我知道我偶然发现了一个正确解决这个问题的 github 包,在浏览了我的搜索历史之后,我终于找到了它。查看 typescript-is - 虽然它需要使用 ttypescript 编译您的代码(我目前正在强迫它使用 create-react-app,稍后将更新成功/失败),您可以做各种疯狂的事情。与 ts-validate-type 不同,该软件包也得到积极维护。

您可以检查某些内容是字符串还是数字并按原样使用它,而编译器不会抱怨:

import { is } from 'typescript-is';

const wildString: any = 'a string, but nobody knows at compile time, because it is cast to `any`';

if (is<string>(wildString)) { // returns true
    // wildString can be used as string!
} else {
    // never gets to this branch
}

if (is<number>(wildString)) { // returns false
    // never gets to this branch
} else {
    // Now you know that wildString is not a number!
}

您还可以检查自己的接口:

import { is } from 'typescript-is';

interface MyInterface {
    someObject: string;
    without: string;
}

const foreignObject: any = { someObject: 'obtained from the wild', without: 'type safety' };

if (is<MyInterface>(foreignObject)) { // returns true
    const someObject = foreignObject.someObject; // type: string
    const without = foreignObject.without; // type: string
}

B
Botond
export interface ConfSteps {
    group: string;
    key: string;
    steps: string[];
}
private verify(): void {
    const obj = `{
      "group": "group",
      "key": "key",
      "steps": [],
      "stepsPlus": []
    } `;
    if (this.implementsObject<ConfSteps>(obj, ['group', 'key', 'steps'])) {
      console.log(`Implements ConfSteps: ${obj}`);
    }
  }
private objProperties: Array<string> = [];

private implementsObject<T>(obj: any, keys: (keyof T)[]): boolean {
    JSON.parse(JSON.stringify(obj), (key, value) => {
      this.objProperties.push(key);
    });
    for (const key of keys) {
      if (!this.objProperties.includes(key.toString())) {
        return false;
      }
    }
    this.objProperties = null;
    return true;
  }

虽然此代码可能会回答问题,但提供有关此代码为何和/或如何回答问题的额外上下文可提高其长期价值。
A
Arnold Vakaria

另一种解决方案可能类似于 HTMLIFrameElement 接口中使用的解决方案。如果我们知道另一个模块中有一个实现,我们可以通过接口创建一个对象来声明一个具有相同名称的变量。

declare var HTMLIFrameElement: {
    prototype: HTMLIFrameElement;
    new(): HTMLIFrameElement;
};

所以在这种情况下

interface A {
    member:string;
}

declare var A : {
    prototype: A;
    new(): A;
};

if(a instanceof A) alert(a.member);

应该可以正常工作


D
Dharman

这个答案很简单。然而,这种解决方案至少在 3/4 的情况下是可能的(尽管并不总是理想的)。因此,换句话说,这可能与正在阅读本文的人有关。

假设我有一个非常简单的函数需要知道参数的接口类型:

const simpleFunction = (canBeTwoInterfaces: interfaceA | interface B) => { 
  // if interfaceA, then return canBeTwoInterfaces.A
  // if interfaceB, then return canBeTwoInterfaces.B
}

获得最多支持的答案往往是使用“功能检查”。 IE,

const simpleFunction = (canBeTwoInterfaces: interfaceA | interface B) => { 
  if (canBeTwoInterfaces.onlyExistsOnInterfaceA) return canBeTwoInterfaces.A
  else return canBeTwoInterfaces.B
}

但是,在我正在使用的代码库中,我需要检查的接口主要包含可选参数。另外,我团队中的其他人可能会在我不知情的情况下突然更改名称。如果这听起来像您正在使用的代码库,那么下面的函数会更安全。

就像我之前说的,这可能会让很多人觉得这是一件非常明显的事情。尽管如此,要知道何时何地应用一个给定的解决方案并不明显,无论它是否恰好是像下面这样一个非常简单的解决方案。

这就是我要做的:

const simpleFunction = (
  canBeTwoInterfaces: interfaceA | interface B,
  whichInterfaceIsIt: 'interfaceA' | 'interfaceB'
) => { 
  if (whichInterfaceIsIt === 'interfaceA') return canBeTwoInterface.A
  else return canBeTwoInterfaces.B
}

E
Evan Crooks

您还可以向子组件发送多个输入,一个是鉴别器,另一个是实际数据,并像这样检查子组件中的鉴别器:

@Input() data?: any;
@Input() discriminator?: string;

ngOnInit(){
    if(this.discriminator = 'InterfaceAName'){
      //do stuff
    }
    else if(this.discriminator = 'InterfaceBName'){
      //do stuff
    }
}

显然,您可以将它移动到任何适用的地方,例如 ngOnChanges 函数或 setter 函数,但这个想法仍然有效。如果您想要反应式表单,我还建议您尝试将 ngModel 绑定到输入数据。您可以使用这些 if 语句根据传入的数据设置 ngModel,并将其反映在 html 中:

<div [(ngModel)]={{dataModel}}>
    <div *ngFor="let attr of (data | keyvalue)">
        <!--You can use attr.key and attr.value in this situation to display the attributes of your interface, and their associated values from the data -->
    </div>
</div>

或者这个代替:

<div *ngIf = "model == 'InterfaceAName'">
    <div>Do This Stuff</div>
</div>
<div *ngIf= "model == 'IntefaceBName'">
    <div>Do this instead</div>
</div>

(在这种情况下,您可以使用 attr.key 和 attr.value 来显示您的界面的属性,以及它们从数据中的关联值)

我知道这个问题已经得到解答,但我认为这可能对试图构建半模糊角度形式的人有用。您还可以将其用于角度材料模块(例如对话框),通过数据参数发送两个变量 - 一个是您的实际数据,另一个是鉴别器,并通过类似的过程对其进行检查。最终,这将允许您创建一个表单,并围绕流入其中的数据塑造表单。


注意:在底部的场景中,您将拥有一个与 getter/setter 或 typescript 文件中的变量相关联的模型变量。
t
troYman

使用字符串文字很困难,因为如果您想重构方法或接口名称,那么您的 IDE 可能不会重构这些字符串文字。如果界面中至少有一种方法,我会为您提供我的解决方案

export class SomeObject implements interfaceA {
  public methodFromA() {}
}

export interface interfaceA {
  methodFromA();
}

检查对象是否属于接口类型:

const obj = new SomeObject();
const objAsAny = obj as any;
const objAsInterfaceA = objAsAny as interfaceA;
const isObjOfTypeInterfaceA = objAsInterfaceA.methodFromA != null;
console.log(isObjOfTypeInterfaceA)

注意:即使我们删除 'implements interfaceA',我们也会得到 true,因为该方法仍然存在于 SomeObject 类中


J
Jack Miller

具有与 selected solution 相同的缺点的简单解决方案,但此变体捕获 JS 错误,仅接受对象作为参数,并具有有意义的返回值。

interface A{
    member:string;
}

const implementsA = (o: object): boolean => {
    try {
        return 'member' in o;
    } catch (error) {
        return false;
    }
}

const a:any={member:"foobar"};

implementsA(a) && console.log("a implements A");
// implementsA("str"); // causes TS transpiler error

“并且有一个有意义的返回值”在什么方面一个布尔返回值比类型保护更好,就像它在所选解决方案中使用的那样?使用您的解决方案,如果我想对对象执行任何特定操作,我将不得不无缘无故地进行类型断言。
“有意义”是指您肯定会获得可靠的返回值,而无需处理错误。根据您的用例,这可能有价值或没有价值。
L
Ledom

这是我使用类和 lodash 提出的解决方案:(它有效!)

// TypeChecks.ts
import _ from 'lodash';

export class BakedChecker {
    private map: Map<string, string>;

    public constructor(keys: string[], types: string[]) {
        this.map = new Map<string, string>(keys.map((k, i) => {
            return [k, types[i]];
        }));
        if (this.map.has('__optional'))
            this.map.delete('__optional');
    }

    getBakedKeys() : string[] {
        return Array.from(this.map.keys());
    }

    getBakedType(key: string) : string {
        return this.map.has(key) ? this.map.get(key) : "notfound";
    }
}

export interface ICheckerTemplate {
    __optional?: any;
    [propName: string]: any;
}

export function bakeChecker(template : ICheckerTemplate) : BakedChecker {
    let keys = _.keysIn(template);
    if ('__optional' in template) {
        keys = keys.concat(_.keysIn(template.__optional).map(k => '?' + k));
    }
    return new BakedChecker(keys, keys.map(k => {
        const path = k.startsWith('?') ? '__optional.' + k.substr(1) : k;
        const val = _.get(template, path);
        if (typeof val === 'object') return val;
        return typeof val;
    }));
}

export default function checkType<T>(obj: any, template: BakedChecker) : obj is T {
    const o_keys = _.keysIn(obj);
    const t_keys = _.difference(template.getBakedKeys(), ['__optional']);
    return t_keys.every(tk => {
        if (tk.startsWith('?')) {
            const ak = tk.substr(1);
            if (o_keys.includes(ak)) {
                const tt = template.getBakedType(tk);
                if (typeof tt === 'string')
                    return typeof _.get(obj, ak) === tt;
                else {
                    return checkType<any>(_.get(obj, ak), tt);
                }
            }
            return true;
        }
        else {
            if (o_keys.includes(tk)) {
                const tt = template.getBakedType(tk);
                if (typeof tt === 'string')
                    return typeof _.get(obj, tk) === tt;
                else {
                    return checkType<any>(_.get(obj, tk), tt);
                }
            }
            return false;
        }
    });
}

自定义类:

// MyClasses.ts

import checkType, { bakeChecker } from './TypeChecks';

class Foo {
    a?: string;
    b: boolean;
    c: number;

    public static _checker = bakeChecker({
        __optional: {
            a: ""
        },
        b: false,
        c: 0
    });
}

class Bar {
    my_string?: string;
    another_string: string;
    foo?: Foo;

    public static _checker = bakeChecker({
        __optional: {
            my_string: "",
            foo: Foo._checker
        },
        another_string: ""
    });
}

在运行时检查类型:

if (checkType<Bar>(foreign_object, Bar._checker)) { ... }

C
ChrisW

因为在运行时类型是未知的,所以我编写了如下代码来比较未知对象,不是与类型,而是与已知类型的对象:

创建正确类型的示例对象 指定其中哪些元素是可选的 将您的未知对象与此示例对象进行深度比较

这是我用于深度比较的(与接口无关的)代码:

function assertTypeT<T>(loaded: any, wanted: T, optional?: Set<string>): T {
  // this is called recursively to compare each element
  function assertType(found: any, wanted: any, keyNames?: string): void {
    if (typeof wanted !== typeof found) {
      throw new Error(`assertType expected ${typeof wanted} but found ${typeof found}`);
    }
    switch (typeof wanted) {
      case "boolean":
      case "number":
      case "string":
        return; // primitive value type -- done checking
      case "object":
        break; // more to check
      case "undefined":
      case "symbol":
      case "function":
      default:
        throw new Error(`assertType does not support ${typeof wanted}`);
    }
    if (Array.isArray(wanted)) {
      if (!Array.isArray(found)) {
        throw new Error(`assertType expected an array but found ${found}`);
      }
      if (wanted.length === 1) {
        // assume we want a homogenous array with all elements the same type
        for (const element of found) {
          assertType(element, wanted[0]);
        }
      } else {
        // assume we want a tuple
        if (found.length !== wanted.length) {
          throw new Error(
            `assertType expected tuple length ${wanted.length} found ${found.length}`);
        }
        for (let i = 0; i < wanted.length; ++i) {
          assertType(found[i], wanted[i]);
        }
      }
      return;
    }
    for (const key in wanted) {
      const expectedKey = keyNames ? keyNames + "." + key : key;
      if (typeof found[key] === 'undefined') {
        if (!optional || !optional.has(expectedKey)) {
          throw new Error(`assertType expected key ${expectedKey}`);
        }
      } else {
        assertType(found[key], wanted[key], expectedKey);
      }
    }
  }

  assertType(loaded, wanted);
  return loaded as T;
}

下面是我如何使用它的示例。

在这个例子中,我希望 JSON 包含一个元组数组,其中第二个元素是一个名为 User 的接口的实例(它有两个可选元素)。

TypeScript 的类型检查将确保我的示例对象是正确的,然后 assertTypeT 函数检查未知(从 JSON 加载)对象是否与示例对象匹配。

export function loadUsers(): Map<number, User> {
  const found = require("./users.json");
  const sample: [number, User] = [
    49942,
    {
      "name": "ChrisW",
      "email": "example@example.com",
      "gravatarHash": "75bfdecf63c3495489123fe9c0b833e1",
      "profile": {
        "location": "Normandy",
        "aboutMe": "I wrote this!\n\nFurther details are to be supplied ..."
      },
      "favourites": []
    }
  ];
  const optional: Set<string> = new Set<string>(["profile.aboutMe", "profile.location"]);
  const loaded: [number, User][] = assertTypeT(found, [sample], optional);
  return new Map<number, User>(loaded);
}

您可以在用户定义的类型保护的实现中调用这样的检查。