Interface type check with Typescript

前端 未结 17 1007
灰色年华
灰色年华 2020-11-22 14:09

This question is the direct analogon to Class type check with TypeScript

I need to find out at runtime if a variable of type any implements an interface. Here\'s my

相关标签:
17条回答
  • 2020-11-22 14:23

    Because the type is unknown at run-time, I wrote code as follows to compare the unknown object, not against a type, but against an object of known type:

    1. Create a sample object of the right type
    2. Specify which of its elements are optional
    3. Do a deep compare of your unknown object against this sample object

    Here's the (interface-agnostic) code I use for the deep compare:

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

    Below is an example of how I use it.

    In this example I expect the JSON contains an array of tuples, of which the second element is an instance of an interface called User (which has two optional elements).

    TypeScript's type-checking will ensure that my sample object is correct, then the assertTypeT function checks that the unknown (loaded from JSON) object matches the sample object.

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

    You could invoke a check like this in the implementation of a user-defined type guard.

    0 讨论(0)
  • 2020-11-22 14:26

    Here's another option: the module ts-interface-builder provides a build-time tool that converts a TypeScript interface into a runtime descriptor, and ts-interface-checker can check if an object satisfies it.

    For OP's example,

    interface A {
      member: string;
    }
    

    You'd first run ts-interface-builder which produces a new concise file with a descriptor, say, foo-ti.ts, which you can use like this:

    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" 
    

    You can create a one-liner type-guard function:

    function isA(value: any): value is A { return A.test(value); }
    
    0 讨论(0)
  • 2020-11-22 14:28

    In TypeScript 1.6, user-defined type guard will do the job.

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

    And just as Joe Yang mentioned: since TypeScript 2.0, you can even take the advantage of tagged union type.

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

    And it works with switch too.

    0 讨论(0)
  • 2020-11-22 14:29

    Based on Fenton's answer, here's my implementation of a function to verify if a given object has the keys an interface has, both fully or partially.

    Depending on your use case, you may also need to check the types of each of the interface's properties. The code below doesn't do that.

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

    Example of usage:

    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
    
    0 讨论(0)
  • 2020-11-22 14:32

    You can achieve what you want without the instanceof keyword as you can write custom type guards now:

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

    Lots of Members

    If you need to check a lot of members to determine whether an object matches your type, you could instead add a discriminator. The below is the most basic example, and requires you to manage your own discriminators... you'd need to get deeper into the patterns to ensure you avoid duplicate discriminators.

    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);
    }
    
    0 讨论(0)
  • 2020-11-22 14:32

    typescript 2.0 introduce tagged union

    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;
        }
    }
    
    0 讨论(0)
提交回复
热议问题