Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript check object by type or interface at runtime with typeguards in 2020+

Tags:

typescript

Most of the time for me, dynamic check is needed for verification of fetch response. And i was thinking, can this be done with user defined typeguard in a generic way for any type of object with multiple props and additional checks, so it can be used something like:

Typescript playground.

Here is an example with sample object, but i want a function without it.

// ================= shared exported =================
type Writer = {
  name: string
  age: number
}

type Book = {
  id: number
  name: string
  tags: string[] | null
  writers: Writer[]
}

// function to check object with multiple props general shape, to not do it by hand
function ofType<T>(obj: any): obj is T {
  if (!obj) return false;

   // how to?
   return true // or false 
}

// ================= used and defined in components =================
function isBook(obj: any): obj is Book {
  if (!ofType<Book>(obj)) return false //checking for shape and simple types

  // cheking for specific values and ranges
  if (obj.id < 1) return false 
  if (obj.writers && obj.writers.some(( { age } )=> age < 5 || age > 150)) return false 

  return true
}


const book = {
  id: 1,
  name: 'Avangers',
  tags: ['marvel', 'fun'],
  writers: [ {name: 'Max', age: 25}, {name: 'Max', age: 25}]
}

console.log(isBook(book)) // true or false
like image 303
ZiiMakc Avatar asked Dec 25 '19 16:12

ZiiMakc


People also ask

How do you check if an object is of a certain type in TypeScript?

In Typescript, we have three ways to work with it using: typeof: the keyword helps to check values types, like boolean, string, number, etc. instanceof: the keyword to compare the object instance with a class constructor. type guards: The powerful way to check types using typescript feature language.

Which keyword is used to check if a class satisfies a particular interface?

The java instanceof operator is used to test whether the object is an instance of the specified type (class or subclass or interface).

Does TypeScript check type at runtime?

Code written in TypeScript is checked for errors before it is executed, during compile time.

How do you check if an object is an instance of an interface TypeScript?

Use a user-defined type guard to check if an object implements an interface in TypeScript. The user-defined type guard consists of a function, which checks if the passed in object contains specific properties and returns a type predicate.


2 Answers

There are actually a number of modules which attempt to translate TypeScript type information into runtime information that can be used to validate data structures.

I'll attempt to list and compare the various solutions here. (ordered roughly by how effective/general-purpose I estimate them to be; yes, this is somewhat subjective!)

Core features: (marked with ✔️yes, ❌no, ⚙️partial, ❔unknown)

ts-base TS base: Standard TypeScript types are used as the basis for the type-metadata. (rather than vice-versa)
class Classes: Can generate type-metadata for classes (based on shape, not instanceof), rather than just interfaces.
func Functions: Can generate type-metadata for functions.
guard Type-guards: Includes functions to validate runtime data against the type-metdata. (ie. type-guards)
auto Auto-check: Can automatically generate invocations of the included type-guards.

Solutions

typescript-is

GitHub: 500 NPM: 2,555 (2020-09-30)

Core features: ts-base: ✔️ class: ❌ func: ❌ guard: ✔️ auto: ⚙️
Note: Auto-check marked as partial, since you can add decorators to class-methods (but not standalone functions) to have their argument types checked.

typescript-json-schema (+ a schema validator, eg. ajv)

GitHub: 1,400 NPM: 51,664 (2020-09-30)

Core features: ts-base: ✔️ class: ✔️ func: ❌ guard: ❌ auto: ❌
Pro: Generates valid json-schemas, which have additional uses. (eg. can be used for data validation in other languages)
Con: Requires some manual work to write generated schemas to disk, package them to be available at runtime, and feed them to your selected JSON-schema validator.

typescript-rtti

GitHub: 54 NPM: 648 (2022-03-09)

Core features: ts-base: ✔️ class: ✔️ func: ✔️ guard: ❌ auto: ❌
Pro: Provides rich metadata about the TypeScript types, that is usable for functionality beyond type-guards.

tst-reflect

GitHub: 77 NPM: 79 (2022-03-09)

Core features: ts-base: ✔️ class: ✔️ func: ✔️ guard: ❌ auto: ❌
Pro: Provides rich metadata about the TypeScript types, that is usable for functionality beyond type-guards.

ts-runtime

GitHub: 313 NPM: 96 (2020-09-30)

Core features: ts-base: ✔️ class: ✔️ func: ✔️ guard: ✔️ auto: ✔️
Con: Cannot currently be applied to only specific files or functions; it adds type-guard invocations throughout the project. (but PRs appear welcomed)
Con: Contains the note: "This package is still experimental and resulting code is not intended to be used in production. It is a proof of concept..."

io-ts (alone)

GitHub: 3,600 NPM: 296,577 (2020-09-30)

Core features: ts-base: ❌ class: ❌ func: ❌ guard: ✔️ auto: ❌
Pro: Doesn't require any typescript transformers, webpack plugins, or CLI commands to operate. (it uses "tricks" to infer the TS types from its custom type-definition structure)

io-ts-transformer (extension for io-ts)

GitHub: 16 NPM: 7 (2020-09-30)

Core features: ts-base: ✔️ class: ❌ func: ❌ guard: ✔️ auto: ❌

ts-auto-guard

GitHub: 134 NPM: 46 (2020-09-30)

Core features: ts-base: ✔️ class: ❔ func: ❌ guard: ✔️ auto: ❌
Con: You must add a specific js-doc tag to each interface you want a type-guard generated for. (a hassle, and error prone)

typeonly

GitHub: 25 NPM: 101 (2020-09-30)

Core features: ts-base: ✔️ class: ❔ func: ❌ guard: ✔️ auto: ❌
Con: Cannot generate type-guards for generic types. (see here)

ts-type-checked

GitHub: 13 NPM: 3 (2020-09-30)

Core features: ts-base: ✔️ class: ❔ func: ❌ guard: ✔️ auto: ❌

Not yet evaluated: ts-json-schema-generator, typescript-to-json-schema, gfx/typescript-rtti Excluded (no public repo): typescript-runtime-types

Disclaimer

I am not the creator or maintainer of any of the solutions listed. I created the list to help developers compare the various solutions, on a consistent set of criteria, while adding helpful information such as GitHub stars and NPM weekly downloads. (edits are welcome to periodically keep these values up-to-date -- though remember to change the last-update-time labels!)

For those with enough reputation, feel free to add additional solutions that you come across. (though please try to keep the text for your new entries consistent with the existing ones)

like image 112
Venryx Avatar answered Sep 21 '22 18:09

Venryx


TypeScript's type system is erased when compiled to JavaScript. That implies any effort to use the standard tsc compiler by itself to generate runtime type guards from type or interface definitions will not succeed; there's nothing of these definitions left at runtime for you to use. So ofType<T>() cannot be implemented.

So what can you do?


If you're willing to use some other compilation step in your build system, you can write or use a transformer that makes type guards for you from these definitions before they are erased. For example, typescript-is will do this.


Or you could use class definitions instead; this makes checking easy at runtime (just use instanceof) but the hard part is deserializing JSON into a class instance and catching errors upon deserialization without writing this yourself manually. All this does is move your problem from implementing ofType<Book>(someObj) to implementing myDeserializerFunction(Book, someObj) where Book is a class constructor.

Here at least you can use decorators and class metadata to generate the code needed for programmatic deserialization. You can write this yourself, or use an existing library such as json2typescript.


Finally, you might decide to start with the type guards and let TypeScript infer your type definitions from them. That is, instead of defining Book and hoping to get a type guard bookGuard() from it, you write the type guard bookGuard() and define Book in terms of typeof bookGuard.

This type guard could be built by composing existing simpler type guards together, so it looks more like a declarative type definition than a data-checking function. You can write this yourself, or use an existing library such as io-ts.

For this approach, it's instructive to look at how one might write such a library. Here's one possible implementation:

export type Guard<T> = (x: any) => x is T;
export type Guarded<T extends Guard<any>> = T extends Guard<infer V> ? V : never;
const primitiveGuard = <T>(typeOf: string) => (x: any): x is T => typeof x === typeOf;
export const gString = primitiveGuard<string>("string");
export const gNumber = primitiveGuard<number>("number");
export const gBoolean = primitiveGuard<boolean>("boolean");
export const gNull = (x: any): x is null => x === null;
export const gObject =
    <T extends object>(propGuardObj: { [K in keyof T]: Guard<T[K]> }) =>
        (x: any): x is T => typeof x === "object" && x !== null &&
            (Object.keys(propGuardObj) as Array<keyof T>).
                every(k => (k in x) && propGuardObj[k](x[k]));
export const gArray =
    <T>(elemGuard: Guard<T>) => (x: any): x is Array<T> => Array.isArray(x) &&
        x.every(el => elemGuard(el));
export const gUnion = <T, U>(tGuard: Guard<T>, uGuard: Guard<U>) =>
    (x: any): x is T | U => tGuard(x) || uGuard(x);

Here we are exporting a few type guards and functions which compose existing type guards. The gString(), gNumber(), gBoolean(), and gNull() functions are just type guards, while gObject(), gArray(), and gUnion() take existing type guards to make new type guards out of them. You can see how gObject() takes an object full of type guard properties and makes a new type guard where each property is checked against the corresponding guard. You could add other composition functions like gIntersection() or gPartial(), but the ones here are enough for your example.

Now your Book and Writer definitions look like this (assume the above has been imported as namespace G):

const _gWriter = G.gObject({
    name: G.gString,
    age: G.gNumber,
});
interface Writer extends G.Guarded<typeof _gWriter> { }
const gWriter: G.Guard<Writer> = _gWriter;

const _gBook = G.gObject({
    id: G.gNumber,
    name: G.gString,
    tags: G.gUnion(G.gArray(G.gString), G.gNull),
    writers: G.gArray(gWriter)
})
interface Book extends G.Guarded<typeof _gBook> { }
const gBook: G.Guard<Book> = _gBook;

If you squint at that you'll see that it's analogous to your example Writer and Book definitions. But in our case the fundamental objects are type guards gWriter and gBook and the types Writer and Book are derived from them. And then you can use gBook directly instead of the non-existent ofType<Book>():

const book = JSON.parse('{"id":1,"name":"Avangers","tags":["marvel","fun"],' +
    '"writers":[{"name":"Max","age":25},{"name":"Max","age":25}]}');

if (gBook(book)) {
    console.log(book.name.toUpperCase() + "!"); // AVANGERS!
}

Okay, hope that helps; good luck!

Playground link to code

like image 20
jcalz Avatar answered Sep 17 '22 18:09

jcalz