简体   繁体   中英

How do I use TypeScript to validate that an object has no overlapping properties with an interface?

I have an interface V and an object x whose type is unknown. If any property of x is a key of V , I want to throw a runtime error. What is the most TypeScript friendly way of doing this?

Here's a clear example:

interface V {
  a: number;
  b: string;
}

const x = getDataFromRequest();

if (hasInvalidProperties(x)) {
  throw new Error();
}

I know there are a million ways to do this using JavaScript (eg- keeping an array of invalid strings, etc) though I am hoping to leverage the type system to make this check implicit.


EDIT: My concern about using an array of invalid keys is that I don't want to forget to update the array if I change the interface. If I use an array, I need a type-checked way to ensure that it stays consistent with the interface.

Once you accept the sad truth that you'll need to maintain some runtime artifact related to V , since V itself will be erased , and assuming you can't actually use the runtime artifact to define V , the best you can do is have the compiler yell at you if your runtime artifact and V are out of sync. Here's one way to do it:

interface V {
    a: number;
    b: string;
}

const keysOfV = ["a", "b"] as const;
type MutuallyAssignable<T extends U, U extends V, V = T> = void;
// the following line will give an error if keysOfV is wrong
type KeysOfVIsGood = MutuallyAssignable<keyof V, typeof keysOfV[number]>; // okay

Note the as const in the definition of keysOfV . That's a const assertion and it (or something like it) is needed to have the compiler keep track of the literal string elements of keysOfV instead of inferring the correct-but-too-wide type string[] .

Then, MutuallyAssignable<T, U> is a type that evaluates to void , but we don't really care about what it evaluates to. What we care about is that T is constrained to U , and U is constrained to T (via a default parameter to sidestep a circular constraint violation). When you use MutuallyAssignable<X, Y> on some types X and Y , you will get a compiler error if the compiler does not recognize that X and Y are mutually assignable.

Then you can go on to define and use your hasInvalidProperties() function however you want, using keysOfV . Perhaps like this:

function hasInvalidProperties(x: object): x is { [K in keyof V]: Record<K, any> }[keyof V] {
    return Object.keys(x).some(k => hasInvalidProperties.badKeySet.has(k));
}
hasInvalidProperties.badKeySet = new Set(keysOfV) as Set<string>;

/// test

function getDataFromRequest(): object {
    return Math.random() < 0.5 ? { c: "okay" } : { a: "bad" };
}
const x = getDataFromRequest();
if (hasInvalidProperties(x)) {
    console.log("not okay");
    throw new Error();
}
console.log("okay");

The main event though is what happens when keysOfV is wrong. Here's what happens when it's missing an entry:

const keysOfV = ["a"] as const;
type MutuallyAssignable<T extends U, U extends V, V = T> = void;
// the following line will give an error if keysOfV is wrong
type KeysOfVIsGood = MutuallyAssignable<keyof V, typeof keysOfV[number]>; // error!
// "b" is not assignable to "a" ------> ~~~~~~~

And here's what happens when it has an extra entry:

const keysOfV = ["a", "b", "c"] as const;
type MutuallyAssignable<T extends U, U extends V, V = T> = void;
// the following line will give an error if keysOfV is wrong
type KeysOfVIsGood = MutuallyAssignable<keyof V, typeof keysOfV[number]>; // error!
// "c" is not assignable to "a" | "b" ---------> ~~~~~~~~~~~~~~~~~~~~~~

Hopefully those error messages and locations are descriptive enough for you to understand how to fix it when V changes.


Okay, hope that helps; good luck!

Link to code

If you're willing to use transformers , you could get the list of properties on an interface at compile time, which would then resolve to a runtime list of strings. Because it relies on a compiler plugin (written in Typescript), it isn't a native TypeScript solution, but it would keep you from having to duplicate data from an interface into a list of strings.

See details on this answer.

There is no way to do this with typescript, because by the time your code gets run, there is no longer any knowledge of your types. The only way to do this is with other tools.

One system we use is json-schema. We write schemas for everything going in and out of APIs, and we use tools to automatically convert JSON-Schema into Typescript types. We don't do the reverse because json-schema is more expressive than typescript.

This way we get both runtime validation and static typing using 1 canonical source.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM