TypeScript struggles with accurately inferring type when a type alias is utilized

Within the code snippet provided, there are two instances of a Record<string, string[]>. One declares itself directly as the Record, while the other uses a type alias that refers to the same Record type. When these are passed into a function along with an empty array (expected to be inferred as an empty string array by TypeScript), the first instance compiles successfully, but the second does not. This issue seems to arise from the type alias causing a problem. Is there a workaround for this situation? Should this be reported as a bug in TypeScript?

function foo<T>(record: Record<string, T>, entity: T) {
}

type StringArrayRecord = Record<string, string[]>;

function test() {
    const working: Record<string, string[]> = {};
    foo(working, []); // Compiles

    const broken: StringArrayRecord = {};
    foo(broken, []); // Does not compile
}

The error message is as follows:

Argument of type 'StringArrayRecord' is not assignable to parameter of type 'Record<string, never[]>'.
  'string' index signatures are incompatible.
    Type 'string[]' is not assignable to type 'never[]'.
      Type 'string' is not assignable to type 'never'.

If an alternate type is used instead of string[] as the second type parameter in the record, the same issue occurs whenever there is ambiguity in the type (such as a union type), for example:

type StringOrNumberRecord = Record<string, string | number>;

function test() {
    const working: Record<string, string | number> = {};
    foo(working, "string"); // Compiles

    const broken: StringOrNumberRecord = {};
    foo(broken, "string"); // Does not compile
}

(This behavior persists in both TypeScript 4.8 and 5.)

(One potential solution is to explicitly specify the type of the second parameter when calling foo, such as

foo(broken, "string" as string | number);
. Another option is to specify the generic type parameter when invoking foo, like
foo<string | number>(broken, "string")
. However, it seems that neither of these should be necessary.)

Answer №1

It seems that everything is functioning as expected, based on information from microsoft/TypeScript#54000. It does appear odd, however, when a type alias has such a significant impact on inference.

The discrepancy arises because, in TypeScript, when comparing two types, the compiler often needs to recursively match various components of each type before determining if they are identical, different, or if one can be used to infer parts of the other.

If both types are defined using an identical generic type with known variance in its type parameter (referenced in this article about variance in TypeScript: Difference between Variance, Covariance, Contravariance, Bivariance and Invariance in TypeScript), then the compiler can optimize by simply comparing the type arguments. For instance, if it's comparing F<string> to F<unknown> for the following F<T> type:

type F<T> = {
  prop: T;
  otherProp: string;
  anotherProp: number;
  // ... additional properties not dependent on T
}

which the compiler has determined to be covariant in its type parameter, it can directly conclude that F<string> is a subtype of F<unknown>, without needing to fully evaluate and compare them.

This shortcut essentially provides a quicker means to reach the same result obtained through a more extensive evaluation process.


Conversely, when the compiler compares F<string> to G where G is defined as:

type G = F<unknown>

it would need to evaluate the types further before making a comparison. By substituting string into F and then evaluating G, it becomes evident that a full comparison of properties in both types is required.

Eventually, it will determine that F<string> is a subtype of G, but via a different route.

(You could also replace F<T> with Record<string, T> and G with Record<string, string[]> here.)


Although these approaches yield similar outcomes, there are observable distinctions. One notable difference is its impact on generic type argument inference.

For instance, when inferring Record<string, T> from a value of Record<string, string[]> while simultaneously inferring T from a value of never[], the compiler utilizes the variance marker on Record's second type parameter to prioritize inferring string[] over never[] due to assignability.

In contrast, when inferring Record<string, T> from a value of type StringOrNumberRecord alongside inferring T from never[], the compiler cannot utilize variance markings as a shortcut. It must fully evaluate

StringOrNumberRecord</code, leading to higher priority for the direct inference candidate of <code>never[]
.

While this inconsistency may seem unfortunate, neither approach is inherently "wrong." According to comments from the TS team dev lead, a more consistent solution would involve reducing the variance marker priority, potentially resulting in both calls failing – a scenario likely unsatisfactory to all parties involved. Therefore, the current behavior stands as is for now.

Answer №2

The main problem arises from the lack of specification for the type T in the function foo when it is called:

// The generic type T is defined here
function foo<T>(record: Record<string, T>, entity: T) {
}

// The variable 'broken' has the type Record<string, string[]>
const broken: StringArrayRecord = {};

foo(broken, []); // Compilation fails due to unknown type for []

Can you spot the issue? The second parameter passed as [] results in an empty and untyped array in Typescript, leading to a type mismatch.

If you replace the empty array with an empty string like this:

foo(broken, [""]);

The compiler can correctly infer the parameters as string[], resolving the issue smoothly.

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

Using TypeScript: creating functions without defining an interface

Can function props be used without an interface? I have a function with the following properties: from - HTML Element to - HTML Element coords - Array [2, 2] export const adjustElements = ({ from, to, coords }) => { let to_rect = to.getBoundingC ...

Integrating TypeScript into an established create-react-app project

Struggling to integrate TypeScript into an existing create-react-app? I've always added it at the beginning of a project using create-react-app my-app --scripts-version=react-scripts-ts, but that's not working this time. The only "solution" I co ...

The Array of Objects is not being generated from Action and Effects

I'm attempting to retrieve an array of objects stored in the User model. I've created both an Action and an Effect for this purpose. The structure of the User Model is as follows: export interface User { _id: string, firstName: string, lastName: ...

I am trying to figure out how to send a One Signal notification from my Ionic app using the One Signal REST API. I have reviewed the documentation, but I am still unable to understand it

Is there a way to send a one signal notification from my ionic app using the one signal API? I have reviewed the documentation but am having trouble with it. While I have set up the service and it is functional, I can only manually send notifications thr ...

Utilizing .js file alongside declaration files .d.ts in Angular: A guide

I am facing an issue with my Angular 7 app where I need to include some typed JS constants from outside of the project. These constants are essential for the AngularJS app and need to be kept in a separate js file. I have defined a new path in the tsconfig ...

What is the best way to sort through custom components within children in Solid JS?

I have been working on developing a scene switcher for my personal application, and I thought of creating a custom component called SceneSwitcher. Inside this component, I plan to add various scenes that can be rendered. Here's an example: <SceneSw ...

Adding new information to a list when a button is clicked: Combining Angular TypeScript with Laravel

Upon entering a reference ID in my system, it retrieves the corresponding record from the database. However, I am facing an issue where adding a new reference number overrides the existing record instead of appending it to the list. https://i.sstatic.net/ ...

Each professional has their own unique workdays and hours, which are easily managed with the angular schedule feature

My schedule is quite dynamic, with professionals attending on different days for varying periods of time. Each professional has a different start and end time on their designated day. To see an example of this setup, you can visit the following link: Sche ...

Exploring TypeScript's Classes and Generics

class Person { constructor(public name: string) {} } class Manager extends Person {} class Admin extends Person {} class School { constructor(public name: string) {} } function doOperation<T extends Person>(person: T): T { return person; } ...

The function Array.foreach is not available for type any[]

I'm encountering an issue where when I attempt to use the ".forEach" method for an array, an error message stating that the property 'forEach' does not exist on type 'any[]' is displayed. What steps can I take to resolve this probl ...

React development: How to define functional components with props as an array but have them recognized as an object

While trying to render <MyComponent {...docs} />, I encountered the following error: TypeError: docs.map is not a function Here's how I am rendering <MyComponent /> from a parent component based on a class: import * as React from &apo ...

When quickly swiping, the button may not respond to the initial click

I'm currently developing an angular application that features a responsive navigation using the angular navbar. To ensure smooth navigation, I have implemented swipe gestures by utilizing hammer.js. Swiping right reveals the menu, while swiping left h ...

Reselect.createSelector() function in Typescript compiler returns incorrect type definition

It seems that the .d.ts file for reselect (https://github.com/reactjs/reselect) is accurate. So why am I experiencing issues here... could it be a problem with the Typescript compiler or my tsconfig? To replicate the problem: Demo.ts import { createSele ...

The specified object is not extensible, hence the property effectTag cannot be added

Upon launching the React application, it initially renders perfectly, but after a few seconds, an error occurs that I am unable to debug. The error is being shown in node_modules/react-dom/cjs/react-dom.development.js:21959. Can anyone provide assistance ...

There was an error encountered: Uncaught TypeError - Unable to access the 'append' property of null in a Typescript script

I encountered the following error: Uncaught TypeError: Cannot read property 'append' of null in typescript export class UserForm { constructor(public parent: Element) {} template(): string { return ` <div> < ...

Declaration File for TypeScript External Node Module Typings

In my Node.js module (index.js), I have transpiled the code using Babel and it looks like this: // My code ... var fs = require("fs"); var path = require("path"); exports.fileContent = fs.readFileSync( path.join(__dirname, "file.txt"), 'utf8&apo ...

showing the current time and including extra minutes

Here is a two-part question for you: I am trying to figure out how to set the current date and time in the format 'dd/MM/yyyy hh:mm a'. It seems to be missing from the code I currently have. If I select 'Add 15 mins' from the drop ...

Oops, it seems like there was an issue with NextJS 13 Error. The createContext functionality can only be used in Client Components. To resolve this, simply add the "use client" directive at the

**Issue: The error states that createContext only works in Client Components and suggests adding the "use client" directive at the top of the file to resolve it. Can you explain why this error is occurring? // layout.tsx import Layout from "./componen ...

What is causing the large build size when using Webpack with Vue.js?

After cloning the repository at: https://github.com/Microsoft/TypeScript-Vue-Starter I executed npm scripts: npm install npm run build The outcome: the size of build.js file is approximately 1MB. Can anyone explain why the build.js file is significant ...

Subscriber fails to receive updates from Behavior subject after calling .next(value)

I am facing an issue where a Behavior Subject does not update a subscriber upon the .next(value) call. Being new to typescript, I believe I might be overlooking something small. The implementation seems correct based on what I have researched. Although t ...