What is the most secure method for setting object references in TypeScript while maintaining type safety?

I am looking to connect objects in a precise and type-safe manner, with the added benefit of type-safe autocompletion.

For example:

type Animal = {
    owner1: Person,
    owner2: Person,
    numberOfLegs: number
}

type Person = {
    animal1: Animal,
    animal2: Animal,
    name: string
}

let animal : Animal = ...
let person : Person = ...

As I write a line like this:

// wire(object1, object2, field1, field2) does
//   object1.field1 = object2
//   object2.field2 = object1
wire(animal, person,...

I expect my IDE to provide autocomplete assistance, ensuring that the 3rd parameter is limited to "owner1" or "owner2" and not "numberOfLegs." Since TypeScript can infer that person has type Person, it should only allow fields that match 'owner1' and 'owner2' from Animal's type, as 'numberOfLegs' has a different type.

While attempting something like the following code snippet might seem promising, unfortunately it doesn't function as intended:

function wire<
  T1,
  T2,
  F1 extends keyof T1,
  F2 extends keyof T2,
  S1 extends F1 & (T1[F1] extends T2 ? F1 : never),
  S2 extends F2 & (T2[F2] extends T1 ? F2 : never),
>(
  o1: T1,
  o2: T2,
  f1: S1,
  f2: S2,
)
{
  o1[f1] = o2
  o2[f2] = o1
  return
}

Answer №1

I won't stress about validating types within the execution of wire(), as I believe your primary concern lies in the behavior from the point of view of the caller. To address this, the implementation may require some type assertions or similar techniques to handle errors, given the complexity of the type logic involved here. Refer to microsoft/TypeScript#48992 for a proposed feature that could facilitate type checking during implementation.

So, let's outline one possible call signature for wire():

declare function wire<T1, T2>(
    o1: T1, o2: T2, 
    k1: KeysMatchingForWrites<T1, T2>, 
    k2: KeysMatchingForWrites<T2, T1>
): void;

type KeysMatchingForWrites<T, V> =
    keyof { [K in keyof T as [V] extends [T[K]] ? K : never]: any };

The

KeysMatchingForWrites<T, V>
type identifies properties of type T where a value of type
V</code can be safely written. This is achieved through <a href="https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as" rel="nofollow noreferrer">key remapping</a>, focusing on keys where <code>V
is a subtype of the property type T[K] (using the conditional type [V] extends [T[K]] ? K : never). Enclosing V and T[K] within brackets prevents the distribution of their types.

Let's test its functionality:

wire(animal, person, "owner1", "animal1");
// function wire<Animal, Person>(
//   o1: Animal, o2: Person, k1: "owner1" | "owner2", k2: "animal1" | "animal2"
// ): void

Seems promising. The type for k1 is "owner1" | "owner2", and for k2 it's

"animal1" | "animal2"</code – exactly as intended. Both <code>numberOfLegs
and name are appropriately rejected.

Notably,

KeysMatchingForWrites<T, V>
verifies that V serves as a subtype of
T[K]</code, not the other way around. The emphasis lies on safe writing rather than reading. For instance:</p>
<pre><code>interface SuperFoo { u: string; }
interface Foo extends SuperFoo { v: Bar }
interface SubFoo extends Foo { w: string; }
declare const foo: Foo;

interface Bar {
    x: SuperFoo, // broader inclusion is acceptable
    y: Foo,
    z: SubFoo; // narrower inclusion is not permitted
}
declare const bar: Bar;

// function wire<Foo, Bar>(o1: Foo, o2: Bar, k1: "v", k2: "x" | "y"): void
wire(foo, bar, "v", "x"); // valid, assigning Foo to SuperFoo is possible
wire(foo, bar, "v", "y"); // fine, assigning Foo to Foo is acceptable
wire(foo, bar, "v", "z"); // error, cannot necessarily assign Foo to SubFoo

In the above example, you assign foo.v to the k2 property of bar. Since foo.v corresponds to type Foo, it can only be safely assigned to bar.x and bar.y. As bar.y is of type SuperFoo</code and all <code>Foo instances are also SuperFoo, this assignment is allowed. However, trying to write to bar.z is disallowed since it requires a SubFoo, which not every Foo object embodies.

Link to code on Playground

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

A TypeScript Record that allows for inferable value types

How can I construct a map that enforces the presence of all keys while still allowing the inference of the types of its values? If I have certain keys, for example: type State = "OPEN" | "CLOSED"; Method 1: using an untyped object con ...

How to Utilize an Array from Observable Object in Angular 2 with ngFor and Async Pipe

I am currently learning about the utilization of Observables in Angular 2. Here is a snippet of code from a service I am working with: import {Injectable, EventEmitter, ViewChild} from '@angular/core'; import {Observable} from "rxjs/Observab ...

Why styled-components namespace problem in React Rollup build?

I designed a "UI Library" to be utilized in various projects using ReactJS + TypeScript + styled-components and Rollup. However, I am currently encountering issues with conflicting classNames. I am aware that styled-components offers a plugin for creating ...

TS Mapped Type: Dynamically exclude specific keys based on their values

Seeking a method to create a mapped type that excludes specific keys based on the value of the mapped key. For instance: Consider an option: Options struct, where Options is a union type defined as: { type: DataType } or { type: DataType, params: DataPar ...

Tips for successfully importing $lib in SvelteKit without encountering any TypeScript errors

Is there a way to import a $lib into my svelte project without encountering typescript errors in vscode? The project is building and running smoothly. import ThemeSwitch from '$lib/ThemeSwitch/ThemeSwitch.svelte'; The error message says "Canno ...

Ways to reset an input field when focused

Looking to implement a number input field in React that clears the initial value when the user clicks on it. While there are solutions available for text inputs, I have not come across a method for number inputs. Every attempt I make at solving this issu ...

Using Angular 6 to implement a select dropdown within a *ngFor loop in

My Angular 6 component fetches an observable list of users and displays a table using the *ngFor statement. Each row has a submit button that calls a function passing in the user for that row. This functionality works correctly. Now, I want to add a selec ...

The VueJS function is not defined

Looking for a way to fetch data from graphql in my vue project and store it in a variable. The function is asynchronous and the value of rawID needs to be awaited. However, there is a possibility that it could result in undefined, causing an error in the ...

Include a new course based on a specific situation

Is it possible to conditionally add a specific class using vue js? In my DataStore, I have two values defined in TypeScript: value1: 0 as number, value2: 0 as number Based on the values of value1 and value2, I want to apply the following classes in my te ...

Angular's custom reactive form validator fails to function as intended

Struggling to incorporate a customized angular validator to check for date ranges. The validator is functioning correctly and throwing a validation error. However, the issue lies in the fact that nothing is being displayed on the client side - there are n ...

Finding the perfect pairing: How to align counters with objects?

public counter = 0; x0: any; x1: any; x2: any; x3: any; x4: any; next(){ this.counter += 1; this.storage.set("Count", this.counter); console.log(this.counter); this.logic(); } logic(){ //automatic counter here var xNum = JSON.parse(JSON.stri ...

The data type 'string' cannot be assigned to the type 'Message' in NEXT.JS when using TypeScript

Currently, I am undertaking the task of replicating Messenger using Next.Js for practice. Throughout this process, I have integrated type definitions and incorporated "Upstash, Serverless access to the Redis database" as part of my project. I meticulously ...

Tips for making a property non-nullable in Typescript

The Node built-in IncomingMessage type from DefinitelyTyped's definition (used as req in the (req, res, next) arguments) has specified that url can be nullable. This excerpt shows part of the definition: // @types/node/index.d.ts declare module "http ...

What is the process for retrieving the API configuration for my admin website to incorporate into the Signin Page?

My admin website has a configuration set up that dynamically updates changes made, including the API. However, I want to avoid hardcoding the base URL for flexibility. How can I achieve this? Please see my admin page with the config settings: https://i.st ...

Utilizing Angular RXJS to generate an array consisting of three different observable streams

I am trying to use three different streams to create an array of each one. For example: [homePage, mainNavigation, loan_originators] However, currently only mainNavigation is being returned. const homePage = this.flamelinkService.getData('homePage ...

Only object types can be used to create rest types. Error: ts(2700)

As I work on developing a custom input component for my project, I am encountering an issue unlike the smooth creation of the custom button component: Button Component (smooth operation) export type ButtonProps = { color: 'default' | 'pr ...

What is the best way to showcase a global variable in Typescript using HTML?

Is there a solution to displaying global variables using the regular {{variable}} bracket method in HTML? Additionally, how can I update the page on HTML to reflect changes made by an external method to this global variable's value? ...

Show a nested JSON object when the specific key is not recognized

I received the following data from my API: { "id": 82, "shortname": "testing2", "fullname": "test2", "address": "addrtest2", "telephone" ...

Having trouble converting the JSON object received from the server into the necessary type to analyze the data

I am new to angular and struggling with converting a JSON object from the server into a custom datatype to render in HTML using ngFor. Any help would be appreciated as I have tried multiple approaches without success. Apologies for the simple HTML page, I ...

Utilizing @types/bootstrap in my Typescript and React project: A comprehensive guide

My project is built using webpack, typescript, and react. I decided to integrate bootstrap by running the following command: yarn add bootstrap @types/bootstrap jquery popper After adding bootstrap, I proceeded to create a component as shown below: imp ...