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

Navigating Divs Using jQuery

I am working with a class that has multiple divs, each with a unique id attached to it. I am using jQuery to dynamically cycle through these divs. This is a snippet of my HTML code: <div id ="result">RESULT GOES HERE</div> ...

jQuery scrollTop(0) leading to unusual scrolling patterns

Every time I click on a button, a modal window appears with a fading effect: $('.display-all-comments').fadeIn(300); $('.display-all-comments').scrollTop(0); If I remove the scrollTop(0), the scrolling works as usual. To better illust ...

Using data from an API, I am implementing JavaScript validation for my dropdown select menu

Using an API, I am able to access information about the city's subway stations through a select option. Currently, I can only display details about one station (Balard). However, I would like to be able to display information about other stations tha ...

How do I incorporate Spotify into my mobile app to enable seamless background music playback?

Currently engaged in a mobile app project that utilizes react-native for the frontend and nodeJS for the backend. The main objective is to enable users to seamlessly play Spotify songs directly within the app, even in the background. This enhancement will ...

Adding a third-party script after closing the body tag on specific pages in NextJS can be achieved by using dynamic imports and

In my NextJS application, a third-party script is currently being loaded on all pages when it's only needed on specific pages. This has led to some issues that need to be addressed. The script is added after the closing body tag using a custom _docum ...

Adjust the color of the entire modal

I'm working with a react native modal and encountering an issue where the backgroundColor I apply is only showing at the top of the modal. How can I ensure that the color fills the entire modal view? Any suggestions on how to fix this problem and mak ...

Different Ways to Access an Array in an EJS Template

After receiving a list of IDs from an API, I need to include them in a URL within an EJS template to retrieve the correct items. For example, the URL format is: Here are some example IDs: 526 876 929 The desired output inside the EJS template: <li&g ...

What is the best way to organize these checkboxes using BootstrapVue's layout and grid system?

My BootstrapVue table setup looks like this: This is the code for the table: window.onload = () => { new Vue({ el: '#app', computed: { visibleFields() { return this.fields.filter(field => field.visible) } ...

What is the reason behind the failure of the cancel test?

I've created two test cases; one for testing the functionality of the Download button and the other for the Cancel button. However, I am encountering issues with the Cancel test failing consistently. Below is the code snippet where I'm attemptin ...

Vue.js component function "not instantiated within the object"

I recently started learning vue.js and decided to use vue-cli to set up a new project. As I was working on adding a method to a component, I encountered some issues: <template> <div> <div v-for="task in $state.settings.subtasks& ...

Combining Vue.js for handling both enter key and blur events simultaneously

I have been working on a solution where pressing the enter key or losing focus on an element will hide it and display a message. However, I am facing an issue where when I press the enter key to hide the element, it also triggers the blur event. I only wan ...

ClickAwayListener is preventing the onClick event from being fired within a component that is nested

I am encountering an issue with the clickAwayListener feature of material-ui. It seems to be disabling the onClick event in one of the buttons on a nested component. Upon removing the ClickAwayListener, everything functions as expected. However, with it e ...

Utilizing a universal JavaScript array within the jQuery document(ready) function

After using jsRender to render the following HTML template, I encountered an issue with passing data values through jQuery selectors when submitting a form via AJAX. <div class="noteActions top" style="z-index: 3;"> <span onclick="noteAction(&a ...

What is the best way to save an image in Node.js?

While I am extracting data from a website, I encountered the need to solve a captcha in order to access further data. I thought of presenting the captcha to the user, but the site is built using PHP and PHP-GD, complicating the process. The URL provided in ...

Is it possible to utilize a const as both an object and a type within TypeScript?

In our code, we encountered a scenario where we had a class that needed to serve as both an object and an interface. The class had a cumbersome long name, so we decided to assign it to a constant. However, when we attempted to use this constant, we faced s ...

Using the Vue.js Compositions API to handle multiple API requests with a promise when the component is mounted

I have a task that requires me to make requests to 4 different places in the onmounted function using the composition api. I want to send these requests simultaneously with promises for better performance. Can anyone guide me on how to achieve this effic ...

React hook form submit not being triggered

import React, { useState } from "react"; import FileBase64 from "react-file-base64"; import { useDispatch } from "react-redux"; import { makeStyles } from "@material-ui/core/styles"; import { TextField, Select, Input ...

Steps for appending a string to a variable

Currently working on creating a price configurator for a new lighting system within homes using Angular 7. Instead of using TypeScript and sass, I'm coding it in plain JavaScript. Page 1: The user will choose between a new building or an existing one ...

jQuery Toggle and Change Image Src Attribute Issue

After researching and modifying a show/hide jQuery code I discovered, everything is functioning correctly except for the HTML img attribute not being replaced when clicked on. The jQuery code I am using: <script> $(document).ready(function() { ...

Is it possible to animate the innerHTML of a div using CSS?

In my HTML file, I have these "cell" divs: <div data-spaces class="cell"></div> When clicked, the innerHTML of these divs changes dynamically from "" to "X". const gridSpaces = document.querySelectorAll("[data-spaces]"); f ...