Mastering Typescript lookup types - effectively limit the properties included in a merge operation with the Partial type

Exploring lookup types, I'm interested in creating a safe-merge utility function that can update an entity of type T with a subset of keys from another object. The objective is to leverage the TypeScript compiler to catch any misspelled properties or attempts to append non-existing ones for T.

For example, I have a Person interface and utilize the built-in Partial type (introduced in v2.1) as follows:

interface Person {
  name: string
  age: number
  active: boolean
}

function mergeAsNew<T>(a: T, b: Partial<T>): T {
  return Object.assign({}, a, b);
}

I then apply this to specific data instances like so:

let p: Person = {
  name: 'john',
  age: 33,
  active: false
};

let newPropsOk = {
  name: 'john doe',
  active: true
};

let newPropsErr = {
  fullname: 'john doe',
  enabled: true
};

mergeAsNew(p, newPropsOk);
mergeAsNew(p, newPropsErr); // <----- I want tsc to yell at me here because of trying to assign non-existing props

The idea is to trigger a TypeScript error on the second invocation since `fullname` and `enabled` are not properties of the `Person` interface. However, while this compiles locally without issues, the output differs when I run it in the online TS Playground.

Bonus question:

When attempting the following assignment:

let x: Person = mergeAsNew(p, newPropsOk);

An error arises specifically in the playground environment stating that `'age' is missing in type '{ name: string; active: boolean; }'. This discrepancy puzzles me as the input arguments align with the `Person` interface structure. Shouldn't `x` be considered of type `Person`?

EDIT: Below is my `tsconfig.json` configuration:

{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "noEmitOnError": true,
    "allowJs": false,
    "sourceMap": true,
    "strictNullChecks": true
  },
  "exclude": ["dist", "scripts"]
}

Answer №1

When you need to indicate that one category possesses only a fraction of characteristics from another category, the traditional method of using extends can also be beneficial

interface Animal {
    species: string
    habitat: string
    carnivorous: boolean
}


let lion: Animal = {
    species: 'lion',
    habitat: 'savannah',
    carnivorous: true
};

let newTraitsValid = {
    species: 'tiger',
    habitat: 'jungle'
};

let newTraitsInvalid = {
  color: 'red',
  size: 'large'
};

// a extends b and not vice versa
// because we want to ensure b does not have traits absent in a
function combineAsNew<T2, T1 extends T2>(a: T1, b: T2): T1 {
    return Object.assign({}, a, b);
}

combineAsNew(lion, newTraitsValid); // valid
combineAsNew(lion, newTraitsInvalid); 
// Argument of type 'Animal' is not compatible 
// with parameter of type '{ color: string; size: string; }'.
//   Property 'color' is missing in type 'Animal'.

PS I am clueless about the current glitch happening in the playground with mapped types

Answer №2

It appears that I was too focused on a particular approach yesterday. After getting some rest, it seems that achieving what I want using Partial may be impractical. Setting aside the peculiar behavior in this scenario, here is my reasoning:

To summarize:

interface Person {
  name: string
  age: number
  active: boolean
}

function mergeAsNew<T>(a: T, b: Partial<T>): T {
  return Object.assign({}, a, b);
}

In the given code, the type Partial<Person> can contain any or none of the properties from Person, making declarations like the following valid for the Partial<Person> type:

let newPropsErr = {
  fullname: 'john doe',
  enabled: true
};

As TypeScript follows structural typing, it only checks if the object's shape matches, disregarding any extra properties. Hence, the shape aligns with Partial<Person>.

The sole way to ensure that arguments match the structure of Partial<Person> and account for any extra properties is by passing them as object literals (which are strictly matched).

Regarding the mergeAsNew function, utilizing Object.assign merges properties from both arguments into a new object comprehensively. Due to TypeScript's compilation-only nature, runtime constraint enforcement for selectively applying b's properties to override a's cannot be implemented.

Referencing @Artem's answer, the use of extend achieves the desired outcome without resorting to mapped or lookup types.

Despite this, the workings of TS Playground still perplex me, leading me to initially believe that such tasks were entirely feasible.

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

Tips for incorporating 'and' in the 'on' clause of 'join' in knex.js

I need assistance implementing the following SQL code in knex.js: select c.id,c.parent_id,c.comment,u.username,c.postid from comments as c join post_details as p on (p.id = c.postid and c.postid=15)join users as u on (u.id = c.userid); I attempt ...

Using a Component's Variable in Another Component in React Native

Hello there! Thank you so much for offering your help. I have a component called PlayerWidget and would like to utilize its state variable isPlaying value in the AlbumHeader Component. Here is the code for the PlayerWidget: import React, { useContext, use ...

Troubleshooting: Angular CLI project encountering issues on Internet Explorer

Today, I encountered an issue when attempting to create a new project using the Angular CLI. An exception is thrown on IE11 and the console displays the following error message: SCRIPT5007: Unable to get property 'call' of undefined or null ref ...

Angular9: construction involves an additional compilation process

After updating my Angular8 project to Angular9, I noticed a new step in the build process which involves compiling to esm. This additional step has added approximately 1 minute to my build time. A snippet of what this step looks like: Compiling @angular/ ...

The benefits of using Node.js for asynchronous calls

Each time a new data is added or existing data is updated, the variables new_data and updated_data will increment accordingly. However, when attempting to output the total count of new_data and updated_data at the end of the code, it always shows as 0. H ...

Version 10.0 of sails is having trouble with an undefined 'schema' when using mysql

i'm currently experimenting with sails js version 0.10.0 using the sails-mysql adapter 0.10.6 I have set up two models: Customer.js module.exports = { connection: 'someMysqlServer', attributes: { name: { type: 'string& ...

Find any consecutive lowercase or uppercase letter and include one more

I have a task in Javascript that I need help with. The goal is to insert a special character between a lowercase and uppercase letter when they are matched together. For example: myHouse => my_House aRandomString => a_Random_String And so on... T ...

Can you create a dynamic visual display using HTML5 Canvas to draw lines in a circular pattern that react

I have successfully implemented drawing lines around a circle using the AudioContext API. However, I am facing an issue with the lineTo function, as the line only grows and does not shrink. My inspiration for this project comes from the audio visualizer fo ...

Emphasizing the text while making edits to an item within the dhtmlx tree

Whenever I need the user to rename an item on the tree, I trigger the editor for them: tree.editItem(tree.getSelectedItemId()); However, I want the text in the editor to be automatically selected (highlighted). Currently, the cursor is placed at the end ...

Parsing Json data efficiently by utilizing nested loops

I have 2 different collections of JSON data, but I'm unsure of how to utilize JavaScript to parse the information. Data from API1 is stored in a variable named response1: [{"placeid":1,"place_name":"arora-square","city":"miami","state":"florida","c ...

Difficulty in connecting React to Node.js with the use of axios

Recently, I embarked on a project using React and Node to create an app that allows users to add people data to a database. The frontend is built with React and can be accessed at localhost:3000, while the backend, developed with Node, runs on localhost:33 ...

Guide to utilizing services in Angular 2

As I've developed a service with numerous variables and functions, my goal is to inject this service into multiple components. Each component should have the ability to update certain variables within the service so that all variables are updated once ...

Creating custom elements for the header bar in Ionic can easily be accomplished by adding your own unique design elements to the header bar or

I'm a beginner with Ionic and I'm looking to customize the items on the header bar. It appears that the header bar is created by the framework within the ion-nav-bar element. <ion-nav-bar class="bar-positive"> <ion-nav-back-button> ...

Translate Firestore value updates into a TypeScript object

Here are the interfaces I'm working with: interface Item { data: string } interface Test { item: Item url: string } In Firestore, my data is stored in the following format: Collection Tests id: { item: { data: " ...

Saving data inputted in a form using ReactJS and JavaScript for exporting later

What is the best way to save or export form input in a ReactJS/JS powered website? For example, if I have a form and want to save or export the data in a format like CSV after the user clicks Submit, what method should I use? Appreciate any advice. Thank ...

Ways to attach an event listener to a useRef hook within a useEffect hook

As I work on creating a custom hook, I am faced with the task of adding an event listener to a ref. However, I am uncertain about how to properly handle cleaning up the event listener since both listRef and listRef.current may potentially be null: const ...

Winston prefers JSON over nicely formatted strings for its output

I have implemented a basic Winston logger within my application using the following code snippet: function Logger(success, msg) { let now = new Date().toUTCString(); let logger = new (winston.Logger)({ transports: [ new (winsto ...

Updating the style of different input elements using Angular's dynamic CSS properties

I am seeking guidance on the proper method for achieving a specific functionality. I have a set of buttons, and I would like the opacity of a button to increase when it is pressed. Here is the approach I have taken so far, but I have doubts about its eff ...

"Unleashing the power of React Native: A single button that reveals three different names

I have a piece of code that changes the name of a button from (KEYWORD) to a different one (KEYNOS) each time it is clicked. How can I modify it to change to a third value (KEYCH), where the default name is (A, B, C... etc), the first click shows Numbers ...

Having trouble implementing font css file in Reactjs

When working with Reactjs (Nextjs), every time I try to incorporate "css" into my project, I encounter the following error on my screen: Module not found: Can't resolve '../fonts/fontawesome-webfont.eot?v=4.7.0' How can I solve this issue? ...