Converting unknown types to interface types in Typescript

I need to create a function called asA that will take a parameter of type unknown and convert it into a specific interface type A, or throw an error if the parameter does not match the interface type A.

The solution needs to be resilient. If I add a new field to interface type A, the compiler should notify me that my function is missing a check for the new field until I update it.

An illustration of such a function asA is provided below, but it currently has issues. The compiler displays the following error:

Element implicitly has an 'any' type because expression of type '"a"' can't be used to index type '{}'. Property 'a' does not exist on type '{}'.(7053)

interface A {
    a: string
}

function asA(data: unknown): A {
    if (typeof data === 'object' && data !== null) {
        if ('a' in data && typeof data['a'] === 'string') {
            return data;
        }
    }
    throw new Error('data is not an A');
}

let data: unknown = JSON.parse('{"a": "yes"}');
let a = asA(data);

Is there a way to correctly implement the function asA according to the specifications mentioned above?

I am open to using typecasts like (data as any)['a'] as long as they prevent silent failures when new fields are added to A.

Answer №1

An alternative to consider is implementing a custom validation solution in TypeScript, tailored to your specific requirements. This approach may require creating a set of type validators for each property and ensuring new fields are properly validated.

  • Validate that a property is of a specified type
  • Ensure all new fields undergo validation

To meet the second requirement, an object mirroring the structure of the main data object can be created with keys matching the properties of the main object, and their respective types defined within it. The resulting type could then be used for verification purposes by iterating through each key and validating its expected type:

interface Data {
  name: string
}

type DataTypes = "string" | "number" | "boolean";
function validateData(data: unknown): Data {
  const keyValidators: Record<keyof Data, DataTypes> = {
    name: "string"
  }
  if (typeof data === 'object' && data !== null) {
    let maybeData = data as Data
    for (const key of Object.keys(keyValidators) as Array<keyof Data>) {
      if (typeof maybeData[key] !== keyValidators[key]) {
        throw new Error('Invalid data format');
      }
    }
    return maybeData;
  }
  throw new Error('Invalid data provided');

}

let input: unknown = JSON.parse('{"name": "John"}');
let result = validateData(input);

We can further enhance this system by developing a generic factory function capable of validating any object type, including additional features like handling optional properties or specifying functions for validation:

interface Data {
  name: string
  optional?: string
}

function allowOptional<T>(as: (s: unknown, errMsg?: string) => T) {
  return function (s: unknown, errMsg?: string): T | undefined {
    if (s === undefined) return s;
    return as(s);
  }
}

function expectString(s: unknown, errMsg: string = ""): string {
  if (typeof s === "string") return s as string
  throw new Error(`${errMsg} '${s} is not a string`)
}

function expectNumber(s: unknown, errMsg?: string): number {
  if (typeof s === "number") return s as number;
  throw new Error(`${errMsg} '${s} is not a number`)
}

type Validators<T> = {
  [P in keyof T]-?: (s: unknown, errMsg?: string) => T[P]
}

function createValidator<T extends object>(keyValidators:Validators<T>) {
  return function (data: unknown, errMsg: string = ""): T {
    console.log(data);
    if (typeof data === 'object' && data !== null) {
      let maybeT = data as T
      for (const key of Object.keys(keyValidators) as Array<keyof T>) {
        keyValidators[key](maybeT[key], errMsg + key + ":");
      }
      return maybeT;
    }
    throw new Error(errMsg + 'Invalid data format');
  }
}

let inputData: unknown = JSON.parse('{"name": "John"}');
const validateInputData = createValidator<Data>({
  name: expectString,
  optional: allowOptional(expectString)
})
let outputData = validateInputData(inputData);

interface MoreData {
  value: Data
}

const validateMoreData = createValidator<MoreData>({
  value: validateInputData
})

let complexData: unknown = JSON.parse('{ "value": {"name": "John"} }');
let processedData = validateMoreData(complexData);
let errorData = validateMoreData(inputData);

Explore Here

Answer №2

Aside from utilizing libraries like ts-json-validator, another option is to employ "user-defined type guards," although this approach may become verbose when handling multiple types.

Using type guards allows you to implement something similar to the following code snippet. It's important to note that while the function returns true or false, its return type is specified as data is A.

interface A {
  a: string
}

function assertIsA(data: unknown): data is A {
  const isA = (typeof data === 'object') && ('a' in (data as any) && typeof (data as any)['a'] === 'string')
  if (isA === false)
    throw new Error('data is not an A');
  return isA
}

let data: unknown = JSON.parse('{"a": "yes"}');

if (assertIsA(data)) { // returns true
  console.log(data.a) // within the conditional data is of type A
}

// all of these throw
console.log(assertIsA(null))
console.log(assertIsA(undefined))
console.log(assertIsA({}))
console.log(assertIsA([]))
console.log(assertIsA({b: 'no'}))
console.log(assertIsA('no'))
console.log(assertIsA(12345))

try it in the playground

If you do not need to explicitly throw errors, the entire function can be condensed into one line:

function assertIsA(data: unknown): data is A {
  return (typeof data === 'object') && ('a' in (data as any) && typeof (data as any)['a'] === 'string')
}

or

const assertIsA = (data: unknown): data is A => (typeof data === 'object') && ('a' in (data as any) && typeof (data as any)['a'] === 'string')

Answer №3

@JulianG's response is helpful, however, as pointed out by @Gezim - the use of 'any' undermines the original intention.

I have come up with an alternative function that employs user-defined type guards to validate the presence of a key. This method also enables the utilization of dot-notations.

function checkKeysExist<T extends string | number | symbol>(input: object, keyName: T | readonly T[]): input is { [key in T]: unknown} {
  let keyNamesArray = Array.isArray(keyName) ? keyName : [keyName];
  let allKeysExist = true;
  keyNamesArray.forEach(aKeyName => {
    if(!(aKeyName in input)) allKeysExist = false;
  });
  return allKeysExist;
}

Usage example:

checkKeysExist(data, 'specificKey')

Or like this:

checkKeysExist(data, ['specificKey_1','specificKey_2'])

Here is the complete script:

interface IObjectWithSpecificKey {
  specificKey: string
}

function checkKeysExist<T extends string | number | symbol>(input: object, keyName: T | readonly T[]): input is { [key in T]: unknown} {
  let keyNamesArray = Array.isArray(keyName) ? keyName : [keyName];
  let allKeysExist = true;
  keyNamesArray.forEach(aKeyName => {
    if(!(aKeyName in input)) allKeysExist = false;
  });
  return allKeysExist;
}

function assertIsObjectWithA(data: unknown): data is IObjectWithSpecificKey {
  const isA = Boolean((typeof data === 'object') && data != null && checkKeysExist(data, 'specificKey') && typeof data.specificKey === 'string');
  return isA;
}

let data: unknown = JSON.parse('{"a": "yes"}');

if (assertIsObjectWithA(data)) { 
  console.log(data.specificKey)
}

console.log(assertIsObjectWithA(null))
console.log(assertIsObjectWithA(undefined))
console.log(assertIsObjectWithA({}))
console.log(assertIsObjectWithA([]))
console.log(assertIsObjectWithA({b: 'no'}))
console.log(assertIsObjectWithA('no'))
console.log(assertIsObjectWithA(12345))
console.log(assertIsObjectWithA({specificKey: 1}))
console.log(assertIsObjectWithA({specificKey: '1'}))

Try it out on the playground here

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Receiving NULL data from client side to server side in Angular 2 + Spring application

I'm currently working on a project that involves using Angular 2 on the client side and Spring on the server side. I need to send user input data from the client to the server and receive a response back. However, I'm encountering an issue where ...

Utilizing module imports as interfaces is a common practice in Typescript

I have a module with several functions defined: export function setNodeCount(count: i32): void { nodeCount = count; } export function getNodeCount(): i32 { return nodeCount; } These functions are imported into another TypeScript file using the follo ...

Discover the method to access the Stacklayout's ID programmatically in NativeScript

As a beginner to nativescript, I am struggling to properly retrieve the stacklayout id. Currently, I am working on Angular2 with typescript and have attempted the following code snippet. Regrettably, I encountered this issue in the command prompt: JS:Erro ...

Fixing Email Validation Error in AngularLearn how to troubleshoot and resolve

I'm encountering an issue when trying to develop an email center using regex pattern, as I'm receiving a validator error in HTML. I have already installed ngx-chips and angular-editor, imported all the necessary modules and dependencies. Here is ...

What is the method for defining specific requirements for a generic type's implementation?

I am facing an issue with the following code snippet, where I am trying to restrict the pairing of Chart objects based on correct types for the data and options objects. However, despite my efforts, the TypeScript compiler is not throwing an error in the s ...

unable to utilize a tip with d3 version 5 in a TypeScript environment?

i'm facing an issue with the following code snippet: var tip = d3.tip() .attr('class', 'd3-tip') .attr('id', 'tooltip') .html(function(d) { return d; }) .direction('n ...

In order to view current data on the chart, I need to use array.slice() in the markup which unfortunately disables the ability to select specific points. Check out the Stackbl

When viewing the line chart, there is a select event that allows for the selection of points and legend items. The line chart includes an activeElements input property that requires an array of the active elements to be passed in. One interesting thing to ...

What is the best way to create a TypeScript function that merges actions together?

I am currently working on a TypeScript function similar to the following: import multipleActionObject from page; it("should be able to perform multiple operations", () => { multipleActionObject.chooseIndex(4).setValue(10); } Inste ...

ngx-datatable is unable to assign a new row model when using [rows]="rows | async"

I am currently working with Angular 2 (version 4.1.0), angularfire2, and ngx-datatable. Within my component, I have a datatable that renders an Observable based on Angularfire2. Users can filter the data in the name column using a similar implementation t ...

Guide on Implementing a Function Post-Rendering in Angular 2+

I'm looking to implement some changes in the Service file without modifying the Component.ts or directive file. Here's what I need: 1) I want to add an event listener after the service renders its content (which is generated by a third-party tool ...

Iterating through the nested JSON causes an error when trying to set properties of undefined

My dataset is structured as a nested JSON, with the main object named bwaResult. Within this object, there are three primary groups: actBwa, fcBwa, and planBwa. Each of these groups contains yearly data and results that include years. I am trying to organi ...

Is it possible to utilize the Angular selector twice while only initializing the component once?

HTML <exchange></exchange> <exchange></exchange> TS @Component({ selector: 'exchange', templateUrl: 'exchange.component.html', }) export class ExchangeCompone ...

Experiencing 429 Too Many Requests error on Angular 7 while attempting to perform multiple file uploads

Whenever I attempt to upload a large number of files simultaneously, I encounter an issue. The API interface only allows for the submission of one file at a time, requiring me to call the service for each individual file. Currently, my code looks like thi ...

Gulp is failing to generate bundled files

I'm having trouble generating my bundle files. Everything was running smoothly until I attempted to update to gulp4, and now that I've reverted back to gulp3, the files are not appearing in my dist directory. Gulp successfully created the files i ...

Select characteristics with designated attribute types

Is there a way to create a type that selects only properties from an object whose values match a specific type? For example: type PickOfValue<T, V extends T[keyof T]> = { [P in keyof (key-picking magic?)]: T[P]; }; I am looking for a solution w ...

Can a TypeScript-typed wrapper for localStorage be created to handle mapped return values effectively?

Is it feasible to create a TypeScript wrapper for localStorage with a schema that outlines all the possible values stored in localStorage? Specifically, I am struggling to define the return type so that it corresponds to the appropriate type specified in t ...

Data from HTML not being transferred by Angular Forms

I am facing an issue with transferring input data from HTML's <select> element to Angular Forms. Let's take a look at my code first. File Name: home-page.component.html <form [formGroup]="rForm" (ngSubmit)="addPaste(rForm.value)"> ...

Creating a return type in Typescript Generics depending on the provided argument

Could you assist in creating a dynamic TypeScript return type based on whether an argument is undefined or not? Here is a export interface ToObject<T> { [k: string]: T; } export const toObject = <T, V = (keyof T)>( list: T[], key: keyof ...

Having trouble uploading images using Ionic/Angular to a PHP script

I've been working on incorporating image uploading functionality into my Ionic app. Despite reading multiple tutorials, I haven't been able to get it up and running successfully. I'm specifically aiming for the app to work smoothly in a web ...

Utilizing React Hooks efficiently with JSDoc annotations and destructuring arrays

Seeking guidance on how to properly JSDoc a React Typescript component that uses hooks, specifically when working with declared destructured arrays. Despite my efforts, I have not been able to find a solution that allows JSDoc to work seamlessly with destr ...