这个问题是使用TypeScript进行类类型检查的直接类比。我需要在运行时找出类型为any的变量是否实现了接口。这是我的代码:

interface A{
    member:string;
}

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

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


如果您在打字稿游乐场输入此代码,则最后一行将被标记为错误,“名称A不存在。当前范围”。但这不是事实,该名称确实存在于当前范围内。我什至可以将变量声明更改为var a:A={member:"foobar"};,而不会引起编辑的抱怨。浏览网络并找到关于SO的另一个问题后,我将接口更改为类,但是随后我无法使用对象文字来创建实例。

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

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


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

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

打字机游乐场的自动完成功能表明,打字机甚至提供了implements方法。如何使用?

评论

JavaScript没有接口的概念,但这不是因为它是一种动态语言。这是因为尚未实现接口。

是的,但是您可以使用类代替接口。请参见此示例。

显然不是在2017年。现在是超级相关的问题。

来自C#背景,撰写本文时的所有解决方案都很糟糕!它涉及复制副本并损害代码的可读性。

#1 楼

无需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);
}


评论


“没有办法在运行时检查接口。”是的,无论出于何种原因,他们都尚未实施。

–trusktr
17-2-16在1:54



如果接口有100个成员,则需要检查所有100个成员? Foobar。

–克里斯蒂安·巴拉(KrisztiánBalla)
17年11月24日在10:43

您可以向对象添加一个鉴别器,而不是全部选中100个...

– Fenton
17年11月24日在11:03

这种鉴别器范式(如此处所述)不支持扩展接口。如果检查派生接口是否是基接口的instanceOf,则它将返回false。

–亚伦
18年5月17日在15:20



@Fenton也许我对此不太了解,但是假设您有一个扩展了接口A的接口B,则希望isInstanceOfA(instantiatedB)返回true,但是希望isInstanceOfB(instantiatedA)返回false。为了使后者发生,B的鉴别者是否不必不是'I-AM-A'?

–亚伦
18年5月18日在17:12



#2 楼

在TypeScript 1.6中,用户定义的类型保护将完成这项工作。

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语句中使用“ object is interface”吗?但是无论如何,非常有趣的语法是我+1。

–lhk
15/12/25在8:36

@lhk没有这样的声明,它更像是一个特殊的类型,它告诉如何在条件分支内缩小类型。由于TypeScript的“范围”,我相信即使在将来也不会有这样的声明。对象与类型之间的另一个不同之处是类型和对象instanceof类是结构化的,它只关心“形状”,而不关心对象从何处获得形状:普通对象或类的实例,它不会没关系。

–vilicvane
15/12/26在9:29

只是为了消除一个误解,这个答案可能会造成:在运行时没有元信息可以推断对象类型或其接口。

–mostruash
16-3-30在22:09



@mostruash是的,答案的后半部分即使在编译时也无法在运行时运行。

–trusktr
17年2月16日在1:58

哦,但是,这必须假定在运行时这些对象将使用type属性创建。在这种情况下,它可以工作。该示例未显示此事实。

–trusktr
17年2月16日下午2:00

#3 楼

打字稿2.0引入标记的联合

打字稿2.0功能

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

–马克拉
16年8月10日在10:40

每晚编译一次,但是intellisense不起作用。它还列出了错误:类型'Square |类型中不存在属性宽度/大小/...。矩形|圈出以防万一。但是可以编译。

–马克拉
16年8月10日在11:12

这实际上只是在使用鉴别器。

– Erik Philips
18年4月16日在20:28

是的这太棒了!如此干净,我喜欢干净简单的方法

–乔治·温贝托·古兹曼·富恩特斯
20 Nov 25 '17:38

并且在创建对象时必须指定其种类?那是不可接受的!

–括号
20 Dec 16'15:39



#4 楼

用户定义的类型防护怎么样? 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,但没有魔术字符串可能会由于缩小而中断。

–斯塔福德·威廉姆斯
16-09-26在9:17

由于某种原因,这对我不起作用,但是(宠物为Fish).swim!== undefined;做到了。

–Cyber​​Mew
19年7月9日在3:05

当您添加swim()时会发生什么?到伯德,因为你有一只宠物鸭?每个宠物都会被视为鱼,不是吗?

–凯兹
20年7月15日在15:52

@Kayz我猜想当您使用isFish时,您的代码实际上并不关心对象是否属于任意鱼类类别,您更关心对象是否支持游泳操作。更好的函数名称可能反映了诸如isAquatic之类的信息。这种用于识别对象类型的方法称为鸭子类型,您可以根据需要进行更多研究。简而言之,如果鸭子会游泳,那么鱼就是鱼,我们要解决一个命名问题。 zh.wikipedia.org/wiki/Duck_typing

– Caleb Macdonald黑色
20年7月16日在0:22

如果“宠物是鱼”,传递鱼或鸟的能力有什么意义?如此糟糕的可读性!

–括号
20 Dec 16'在14:45

#5 楼

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

用法示例:

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

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)
}


用reflec-ts编译并启动它:

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


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

更新:
您可以在此处找到完整的工作示例

评论


我认为这很愚蠢,但是停顿了一秒钟,看了一下您的github页面,发现它保持最新状态并有据可查,所以被否决了:-)我仍然不能证明我现在只是为了使用实现,但想要认识到您的承诺,并且不想成为卑鄙的人:-)

–Simon_Weaver
16年8月23日在9:34

实际上,我看到的这种反射功能的主要目的是创建更好的IoC框架,就像Java世界早已拥有的框架一样(Spring是第一个也是最重要的一个)。我坚信TypeScript可以成为将来最好的开发工具之一,而反射是它真正需要的功能之一。

–pcan
16-8-24在10:11



...呃,那又如何,我们必须将这些编译器的“增强功能”引入未来的Typescript版本中?这实际上是Typescript的分支,而不是Typescript本身,对吧?如果是这样,这不是一个可行的长期解决方案。

– dudewad
16年7月7日在21:01

正如许多其他主题中所述,@ dudewad是一个临时解决方案。我们正在等待通过转换器的编译器可扩展性。请在官方TypeScript存储库中查看相关问题。此外,所有被广泛采用的强类型语言都具有反射性,我认为TypeScript也应该具有反射性。和我一样,许多其他用户也这样认为。

–pcan
16-12-7在21:28



这正是概念验证的目的:向人们证明事情可以完成。问题指出:“我知道javascript作为一种动态语言没有接口的概念。有没有办法对接口进行类型检查?”答案是:否,没有修改/改进,但是如果我们有扩展/改进语言和编译器的方法,则回答是。问题是:谁来决定更改?但这是另一个话题。

–pcan
20年6月4日在8:28

#6 楼

与上面使用用户定义的防护的情况相同,但是这次具有箭头功能谓词

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);


#7 楼

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

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

var obj : any = new Foo();

if (obj.someInterfaceMethod) {
    ...
}


评论


如果形状复杂怎么办?您不想在每个深度级别对每个属性进行硬编码

–汤姆
18年7月4日在23:57

@Tom我想您可以将运行时值或示例/示例(即您想要的接口的对象)传递(作为检查器函数的第二个参数)。然后,而不是硬编码代码,而是编写所需的接口的任何示例,并编写一些一次性的对象比较代码(例如,使用(用于(在obj中的元素){})来验证这两个对象具有相似类型的相似元素。

– ChristW
19年5月8日在13:11



#8 楼

这是另一个选择: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仅指一种类型,但在此处被用作值。返回A.test(value);

–PatricNox
20-10-26在13:06

您必须与示例中的内容有所不同。 const {A} = ...是创建值A的原因。

– DS。
20-10-27在14:05

#9 楼

TypeGuard

interface MyInterfaced {
    x: number
}

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

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


评论


“ arg是MyInterfaced”是一个有趣的注释。如果失败了怎么办?看起来像一个编译时接口检查-刚好是我想要的。但是,如果编译器检查参数,那么为什么根本没有函数体。如果可以进行这种检查,为什么还要将其移至单独的功能。

–lhk
17年6月28日在10:50

@lhk刚刚阅读了有关类型防护的打字稿文档... typescriptlang.org/docs/handbook/advanced-types.html

–德米特里·马特维耶夫(Dmitry Matveev)
17年6月28日在21:13

@DmitryMatveev orrr ...只回答一个完全合理的问题,而不是指向没有这个问题的文档?

–灰
20年8月7日在9:53

@lhk不确定您是否对此还有疑问,但是无论如何,我将尝试实际回答。您是对的,因为它是编译时检查。 arg is MyInterfaced位告诉编译器:“如果分支调用此函数并且结果为true,则接受对被测试为MyInterfaced类型的对象的所有进一步使用。”可以用该语句中的关键位(即“如果结果为真”)突出显示可能引起混乱的原因。不幸的是,这取决于开发人员来确定什么构成MyInterfaced。

–灰
20年8月7日在10:07

我之所以说“不幸”,是因为出于通常确定任何给定对象是否具有任何给定接口类型的目的,这种方法没什么用。

–灰
20年8月7日在10:09

#10 楼

根据Fenton的回答,这是我实现一个函数的功能,以验证给定的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
 


#11 楼

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;
  }


评论


尽管此代码可以回答问题,但提供有关此代码为何和/或如何回答问题的其他上下文,可以提高其长期价值。

– xiawi
19-10-18在7:45

#12 楼

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

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


#13 楼

我在文件@progress/kendo-data-query中找到了来自filter-descriptor.interface.d.ts的示例
检查器
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
    }
});


#14 楼

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


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

这里是(与界面无关的)代码深入比较:

 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的接口的实例(具有两个可选元素)。 -checking将确保我的示例对象正确,然后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);
}

 


您可以邀请在用户定义的类型保护程序的实现中进行这样的检查。

#15 楼

使用字符串文字很困难,因为如果您要重构方法或接口名称,那么您的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”,我们也将实现,因为该方法仍然存在SomeObject类

#16 楼

这是我使用类和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)) { ... }


#17 楼

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();
}