Expand by focusing solely on recognized attributes?

I am working on creating an interface that can accept a mapped type, allowing for both runtime logic and compile-time typing to be utilized.

Here is an example of what I'm aiming for:

type SomeType = {
  a: string
  b: { a: string, b: string }
}
magicalFunction({ a: 1 }) // 1. return type is {a: string}
magicalFunction({ b: 1 }) // 2. return type is { b: { a: string, b: string } }
magicalFunction({ b: { a: 1 } }) // 3. return type is { b: { a: string } }
magicalFunction({ a: 1, c: 1 }) // 4. compile-time error since there's no 'c' on SomeType

(In reality, magicalFunction takes SomeType as a generic parameter. For this discussion, let's assume it's hardcoded with SomeType.)

I've managed to achieve the first three behaviors using mapped types:

export type ProjectionMap<T> = {
  [k in keyof T]?: T[k] extends object
    ? 1 | ProjectionMap<T[k]>
    : 1

export type Projection<T, P extends ProjectionMap<T>> = {
  [k in keyof T & keyof P]: P[k] extends object
    ? Projection<T[k], P[k]>
    : T[k]
}

type SomeType = {
  a: string
  b: { a: string, b: string }
}

function magicalFunction<P extends ProjectionMap<SomeType>>(p: P): Projection<SomeType, P> {
  /* using `p` to do some logic and construct something that matches `P` */
  throw new Error("WIP")
}

const res = magicalFunction({ a:1 })
// etc

The issue I'm facing is that when extra properties are specified, like {a:1, c:1}, there is no compilation error. While this behavior makes sense due to all fields being optional in the inferred P type, I am looking for a solution to enforce stricter typing. Is there a way to achieve this without losing type inference?

It seems that modifying P during type specification for the p parameter disrupts type inference. One potential approach could involve a key filtering type that fails for unknown keys, recursively. Changing the signature to

magicalFunction<...>(p: FilterKeys<P, SomeType>): ... 
results in a call like magicalFunction({a:1}) resolving to magicalFunction<unknown>.

background

My ultimate objective is to create a repository class that is typed to a specific entity and capable of executing projection queries against MongoDB. To ensure type safety, I aim to have auto-complete functionality for projection fields, compilation errors when specifying non-existent fields, and a return type that aligns with the projection.

For instance:

class TestClass { num: number, str: string }
const repo = new Repo<TestClass>()
await repo.find(/*query*/, { projection: { num: 1 } }) 
// compile-time type: { num: number}
// runtime instance: { num: <some_concrete_value> }

Answer №1

In TypeScript, object types are matched using structural subtyping, making them open and extendible. If `Base` is an object type and `Sub extends Base`, then a `Sub` can be used wherever a `Base` is required:

const sub: Sub = thing; 
const base: Base = sub; // okay because every Sub is also a Base

This means that every property of `Base` must be present in `Sub`, but not vice versa. You can add new properties to `Sub` without breaking the subtyping rule:

interface Base {
    baseProp: string;
}
interface Sub extends Base {
    subProp: number;
}
const thing = { baseProp: "", subProp: 123 };

Even with extra properties, `Sub` remains a kind of `Base`. TypeScript's object types are therefore open and extendible by nature.

While there is no support for "exact types" in TypeScript currently, you can use workarounds like self-referencing generic constraints to simulate exact types.

One approach involves the use of utility types like `Exclude` and `Record` to enforce constraints on excess properties within object types. This creates a mechanism similar to exact types in TypeScript:

type Exactly<T, X> = T & Record<Exclude<keyof X, keyof T>, never>;

You can apply this `Exactly` type recursively in generic definitions to prohibit excess keys even in nested object types:

type ProjectionMap<T, P extends ProjectionMap<T, P> = T> = Exactly<{
    [K in keyof T]?: T[K] extends object
    ? 1 | ProjectionMap<T[K], P[K]>
    : 1 }, P>;

This allows you to define types that reject objects with excess properties beyond what is specified by their base type.

By leveraging these techniques, you can achieve a level of strictness in TypeScript object typing that resembles "exact types," ensuring compatibility while guarding against unexpected additional properties.

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

Angular function implementing a promise with a return statement and using the then method

I have a function in which I need to return an empty string twice (see return ''. When I use catch error, it is functioning properly. However, I am struggling to modify the function so that the catch error is no longer needed. This is my current ...

Struggling to fetch information with Angular HttpClient from an API that sends back a JSON response with an array

As a beginner in Ionic and Angular, I am attempting to call an API and then showcase the team names within the template of my project. Despite following numerous tutorials and videos, I seem to be stuck as the JSON response returns an object with results f ...

What sets 'babel-plugin-module-resolver' apart from 'tsconfig-paths'?

After coming across a SSR demo (React+typescript+Next.js) that utilizes two plugins, I found myself wondering why exactly it needs both of them. In my opinion, these two plugins seem to serve the same purpose. Can anyone provide insight as to why this is? ...

Implementing the 'colSpan' attribute in ReactJS

I encountered an error saying "Type string is not assignable to type number" when attempting to include the colSpan="2" attribute in the ReactJS TypeScript code provided below. Any suggestions on how to resolve this issue? class ProductCategoryRow exten ...

What is the best way to shorten text in Angular?

I am looking to display smaller text on my website. I have considered creating a custom pipe to truncate strings, but in my situation it's not applicable. Here's what I'm dealing with: <p [innerHTML]="aboutUs"></p> Due to t ...

In Typescript, encountering a member of a union type with an incompatible signature while utilizing the find method on an array of

I need to verify if a specific value exists within an array of objects. The structure of my array is as follows: [ 0: { id: 'unique_obj_id', item: { id: 'unique_item_id', ... }, ... }, 1: {...} ] The objects in the ar ...

Angular time-based polling with conditions

My current situation involves polling a rest API every 1 second to get a result: interval(1000) .pipe( startWith(0), switchMap(() => this.itemService.getItems(shopId)) ) .subscribe(response => { console.log(r ...

Why is it necessary to omit node_modules from webpack configuration?

Check out this webpack configuration file: module.exports = { mode: "development", entry: "./src/index.ts", output: { filename: "bundle.js" }, resolve: { extensions: [".ts"] }, module: { rules: [ { test: /\.ts/ ...

Encountering TS 2732 error while attempting to incorporate JSON into Typescript

Having trouble importing a JSON file into my TypeScript program, I keep getting error TS2732: Can't find module. The JSON file I'm trying to import is located in the src folder alongside the main.ts file. Here's my code: import logs = requi ...

Having trouble resolving 'primeng/components/utils/ObjectUtils'?

I recently upgraded my project from Angular 4 to Angular 6 and everything was running smoothly on localhost. However, during the AOT-build process, I encountered the following error: ERROR in ./aot/app/home/accountant/customercost-form.component.ngfactory. ...

Displaying a TypeScript-enabled antd tree component in a React application does not show any information

I attempted to convert the Tree example from antd to utilize TypeScript, however, the child-render function does not seem to return anything. The commented row renders when I remove the comment. The RenderTreeNodes function is executed for each element in ...

Seeking guidance for the Angular Alert Service

I'm relatively new to using Angular and I'm struggling to determine the correct placement for my AlertService and module imports. Currently, I have it imported in my core module, which is then imported in my app module. The AlertService functions ...

retrieve the state property from NavLink

I am encountering an issue with passing objects through components in my project. Specifically, I have a chat object within a component that defines a NavLink. When a user clicks on the ChatsElement, which is a link, the page navigates to the URL /friends/ ...

What steps can I take to perform unit testing on a custom element?

While working on a project where I have a class that extends HTMLElement, I came across some interesting information in this discussion: https://github.com/Microsoft/TypeScript/issues/574#issuecomment-231683089 I discovered that I was unable to create an ...

Experimenting with Cesium using Jasmine (Angular TypeScript)

I have a TypeScript app built using Angular that incorporates Cesium: cesium-container.component.ts import { Component, ElementRef } from '@angular/core'; import { Viewer } from 'cesium'; import { SomeOtherCesiumService } from 'sr ...

Enhancing React Flow to provide updated selection and hover functionality

After diving into react flow, I found it to be quite user-friendly. However, I've hit a roadblock while attempting to update the styles of a selected node. My current workaround involves using useState to modify the style object for a specific Id. Is ...

Guide to asynchronously loading images with Bearer Authorization in Angular 2 using NPM

I am in search of a recent solution that utilizes Angular2 for my image list. In the template, I have the following: <div *ngFor="let myImg of myImages"> <img src="{{myImg}}" /> </div> The images are stored as an array w ...

Drizzle ORM retrieve unique string that is not a database column

I'm working with a SQL query that looks like this: SELECT * FROM ( SELECT 'car' AS type, model FROM car UNION SELECT 'truck' AS type, model FROM trucks ) vehicles; In Drizzle, I'm trying to replicate the 'car ...

Error in React Typescript: No suitable index signature with parameter type 'string' was located on the specified type

I have encountered an issue while trying to dynamically add and remove form fields, particularly in assigning a value for an object property. The error message I received is as follows: Element implicitly has an 'any' type because expression o ...

Assign a variable with the value returned by a function

Can you help me with this question I have about validating fields with a function using AbstractControl? errorVar: boolean = false function(c: AbstractControl): {[key: string]: string } | null { // validation if 'test' is true or not goes here ...