Search code examples
typescripttypechecking

How can I do inline type checking without storage in TypeScript?


I have some interface

ITestInterface {
  foo: string;
}

I'd like to pass an instance of this interface as an argument to a function. The function will take any object type, so it does not type check on it's own. In order to make sure an object is of the correct type, I can use storage:

const passMe: ITestInterface = { foo: "bar" };
someFunction(passMe);

But I'd like to have a way to create the argument inline, while still doing type checking.

// made up example syntax
someFunction({ foo: "bar" } istype ITestInterface);

Is there a nice way to something like the example above inline?

I've tried using as, but it doesn't limit the type. For example, the following is valid.

someFunction({ foo: "bar", hello: true } as ITestInterface);

Another thing I can do in this instance is modify someFunction to have templating, but it's not what I'd consider a great solution. I won't always have this privilege.

someFunction<TYPE>(arg: TYPE) {
  // modify function definition
}

someFunction<ITestInterface>({foo: "bar"});

Solution

  • The specific feature you're looking for, something like "type annotations for arbitrary expressions", doesn't exist in TypeScript. There is an open suggestion for it currently marked as "needs proposal", so you might want to give it a 👍 or describe your ideas if they are compelling and different from what's already in there. But it doesn't look to me like anyone's working on it, so I wouldn't hold my breath if I were you.


    There are several ways to go here, each with their own issues.

    As you've seen, the easiest thing to do is to use a type assertion. This works to prevent you from passing in a completely unrelated type:

    // assertion
    someFunction({ foo: "bar" } as ITestInterface); // okay as expected
    someFunction({ unrelatedThing: 1 } as ITestInterface); // error as expected
    

    It also allows extra properties (which is still sound and type safe, an object of type ITestInterface isn't guaranteed not to have other properties... it might surprise you because you expect excess property checking, but those only happen sometime):

    someFunction({ foo: "bar", hello: true } as ITestInterface); // okay by design,
    // excess properties are allowed
    

    But the big dealbreaker here is that type assertions let you unsafely narrow types, so the following will not be an error:

    someFunction({} as ITestInterface); // no error ?! assertions also NARROW types
    

    The other way you could go would be to create a helper function called isType like this:

    // helper function
    const isType = <T>(x: T) => x;
    

    This behaves almost exactly as you'd like:

    someFunction(isType<ITestInterface>({ foo: "bar" })); // okay as expected
    someFunction(isType<ITestInterface>({ unrelatedThing: 1 })); // error as expected
    
    someFunction(isType<ITestInterface>({ foo: "bar", hello: true })); // error as you want
    someFunction(isType<ITestInterface>({})); // error as everyone wants
    

    But, as you said, it might not be worth it to you. Most runtime engines will happily inline functions like x => x so I wouldn't think it's a performance issue. But it might be an elegance issue, which is up to you.


    Anyway, those are the best I can do. Hope that helps. Good luck!

    Link to code