Is it possible to assign a name to an implied return type?

Imagine having a function like this:

function fetchUser() {
  return {
    username: "johndoe",
    email: "johndoe@example.com"
  }
}

You can create a type based on the return value:

type User = ReturnType<typeof fetchUser>;

But, when you hover over the function call or user below:

const user = fetchUser();

they are automatically given the type:

{
  username: string,
  email: string
}

However, it would be more beneficial if they were typed as User. Sometimes, it's important to know that an object was returned by a specific function. This is because functions may only return certain properties of the inferred types, and the type system may not be able to specify constraints like non-negative age.

So, ideally we could specify the User type in a way that when applying it to a function like:

function handleUser(user: User) {
  // ...
}

The following would be accepted:

handleUser(fetchUser());

but this would not:

handleUser({
  username: "johndoe",
  email: "johndoe@example.com"
});

This would result in an error. It's uncertain if TypeScript supports this feature or if it aligns with its design principles, but it would be useful. Is there a way to achieve this?

I have attempted different methods, but they have led to circular definition errors. If this feature does not exist, I will consider making a feature request. I am hoping for a syntax like:

function fetchUser(): infer User {

or

type User = ReturnType<typeof user as const>;

Answer №1

What you're really searching for is a nominal type, where only values of the type named Student (or a type from the same type declaration) are permitted. However, TypeScript's type system follows a largely structural approach rather than a nominal one. If two object types share the same members, they are considered the same type:

interface Student {
    firstName: string;
    lastName: string;
    age: number;
}
const student: Student = {
    firstName: "abc", lastName: "def", age: 123
};

interface Master {
    firstName: string;
    lastName: string;
    age: number;
}
const master: Master = student; // okay
// The student has become the master!!

These distinctions in TypeScript are intentional, unlike in other popular typed languages. If you wish for a different behavior, it's advised to understand and adapt with the system rather than trying to go against it. If there's a need for two types to be distinct, try to represent that difference structurally. Maybe one should have a method that the other doesn't? Finding a structural solution might lead to better outcomes.


Nevertheless, there are instances where nominal types are desired. TypeScript has an ongoing issue at microsoft/TypeScript#202, and many are in favor of it. There are also some techniques available to simulate/emulate nominal types.

One method is to include a "brand" member as detailed in the TypeScript FAQ, which reduces the chances of unintentional matches with other types. However, conflicts can still occur.

For more robust control, you can utilize a class with a private member to achieve this. This creates an "un-copyable" brand; private and protected class members are compared nominally and not structurally. Implementing this approach will align with the desired behavior:

class Student {
    firstName = "John";
    lastName = "Doe";
    age = 18;
    private readonly __type = "Student"
}
function passStudent(student: Student) { }

const student = new Student();
passStudent(student); // okay
passStudent(new Student()); // okay
passStudent({
    firstName: "John",
    lastName: "Doe",
    age: 18,
    __type: "Student"
}); // error!

In this scenario, the passed object literal matches the structure of Student, but the compiler raises an error because __type is private in Student. Even another class with a private member is rejected:

class Impostor {
    firstName = "John";
    lastName = "Doe";
    age = 18;
    private readonly __type = "Student" // 😈
}
passStudent(new Impostor()); // error!

For further exploration, Playground link to code

Answer №2

To ensure the correct type matching in your code, you can use a type assertion to explicitly define the return type of the function getStudent as Student. By doing this, any variable or function parameter defined as Student will only accept values that align with the structure of the Student type.

type Student = {
  firstName: string;
  lastName: string;
  age: number;
}

function getStudent(): Student {
  return {
    firstName: "Alice",
    lastName: "Smith",
    age: 22,
  };
}

function displayStudentInfo(student: Student) {
  console.log(student.firstName, student.lastName);
}

const studentInfo = getStudent() as Student;
displayStudentInfo(studentInfo);

Answer №3

Implementing a wrapper function that seems to work:

function _getStudent() {
  return {
    firstName: "John",
    lastName: "Doe",
    age: 18,
  }
}

type Student = ReturnType<typeof _getStudent> & { __type: "Student" };

function getStudent(): Student {
  const student = _getStudent() as Student
  student.__type = "Student";
  return student;
}

see in playground

Below are the tests conducted:

const _student = _getStudent();
const student = getStudent();
const struct = {
  firstName: "John",
  lastName: "Doe",
  age: 18,
}
const typedStruct = {
  ...struct,
  __type: "Student"
}
const coercedStruct = typedStruct as Student
const propertyCoersion = {
  ...struct,
  __type: "Student" as "Student"
}

function passStudent(student: Student) {
  return true;
}

passStudent(_student);
passStudent(student);
passStudent(struct);
passStudent(typedStruct);
passStudent(coercedStruct);
passStudent(propertyCoersion);

see in playground

Although the tests above pass, it's important to note that the following scenario won't behave as expected:

passStudent({
  firstName: "John",
  lastName: "Doe",
  age: 18,
  __type: "Student",
});

As mentioned by jonrsharpe, the type inference doesn't work as intended in this case. However, combining the wrapper and private class approach as suggested by jcalz in the accepted answer can provide a solution with additional benefits:

This approach allows for encapsulation of validation logic without the need for bulky try-catch blocks:

class JWT {
  private __type = "JWT";
  public header: JWTHeader;
  public payload: JWTPayload;
  public signature: string;
 
  constructor(public token: string) {
    if(!isValidJWT(token)) throw "Invalid JWT";
    //...
  }

  //...

  toString() {
    return this.token;
  }
}

function parseJWT(input: string): JWT | null {
  try {
    return new JWT(input)
  } catch (error) {
    if(error === "Invalid JWT") return null;
    throw error;
  }
}

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

How can one break down enum values in typescript?

I've defined an enum in TypeScript as shown below: export enum XMPPElementName { state = "state", presence = "presence", iq = "iq", unreadCount = "uc", otherUserUnreadCount = "ouc", sequenc ...

How can conditional types be implemented with React Select?

I am working on enhancing a wrapper for React-select by adding the capability to select multiple options My onChange prop is defined as: onChange: ( newValue: SingleValue<Option>, actionMeta: ActionMeta<Option>, ) => void Howev ...

What is the best way to retrieve class properties within an input change listener in Angular?

I am new to Angular and have a question regarding scopes. While I couldn't find an exact match for my question in previous queries, I will try to clarify it with the code snippet below: @Component({ selector: 'item-selector&apos ...

Custom component not rendering expected CSS style

I have successfully developed a custom web component without using any framework. I then proceeded to populate it with content from a template tag. Although I was able to manipulate the content using JavaScript, I encountered difficulties when trying to m ...

Using a custom TypeScript wrapper for Next.js GetServerSideProps

I developed a wrapper for the SSR function GetServerSideProps to minimize redundancy. However, I am facing challenges in correctly typing it with TypeScript. Here is the wrapper: type WithSessionType = <T extends {}>( callback: GetServerSideProps&l ...

Differing preferences for indentation styles can lead to conflicting prett

My eslint setup is as follows: { "env": { "es2020": true, "jest": true }, "extends": [ "eslint:recommended", "plugin:react/recommended", "plugin:import/recommended&q ...

The type undefined cannot be assigned to the type even with a null check

When looking at my code, I encounter an error stating Argument of type 'Definition | undefined' is not assignable to parameter of type 'Definition'. Even though I am checking if the object value is not undefined with if (defs[type] != u ...

One efficient way to iterate through an object and modify its values in a single line of code

_shop: { [key: string]: string[] } = { fruits: ['Apple', 'Orange'], vegetables: ['Tomato', 'Onions'] } Can a one-liner code be used to modify the values of _shop and return it in a specific format? The desired outp ...

When working with Visual Studio and a shared TypeScript library, you may encounter the error message TS6059 stating that the file is not under the 'rootDir'. The 'rootDir' is expected to contain all source files

In our current setup with Visual Studio 2017, we are working on two separate web projects that need to share some React components built with TypeScript. In addition, there are common JavaScript and CSS files that need to be shared. To achieve this, we hav ...

Enhance React components in Deck.GL by including default properties for each child component

I am currently working on a customizable collection of map layers using Deck.GL & React. I have developed a BaseMap component through which I will be passing data layers as react children. This is the current implementation: BaseMap: export const BaseMap ...

How can you determine if an API method call has completed in Angular and proceed to the next task?

Two methods are being used for api calls in my code. Method one is calling out method two and needs to wait for method two's api call to finish before continuing with its own process. I attempted to achieve this using the complete function inside a su ...

PhpStorm does not currently support types in JavaScript

Currently, I am using PhpStorm for a Vue 2 / TypeScript project. However, whenever I attempt to add return types to functions, I encounter the error message "Types are not supported by current JavaScript version": https://i.sstatic.net/ct3gu.png In the " ...

How can I pass properties from a childComponent to a parent component in Angular 2 without prior knowledge of the childComponent's class?

My main goal is to accomplish the following : I currently have a component setup like this: import { Component, Output, EventEmitter, OnInit } from '@angular/core'; @Component({ selector: 'like', template: '<p>this is ...

What is the best method for accessing a store in Next.js with Redux Toolkit?

Currently, I am working on incorporating integration testing for my application using Jest. To achieve this, I need to render components in order to interact with various queries. However, in order to render a component, it must be wrapped in a Provider to ...

Leverage the new Animation support in RC 5 to animate each item in an *ngFor list sequentially in Angular 2

I have a unique component that retrieves a list of items from the server and then displays that list using *ngFor in the template. My goal is to add animation to the list display, with each item animating in one after the other. I am experimenting with t ...

Unable to pass response from httpclient post method to another custom function in Angular 4

I've implemented the addUser(newUser) function in my sign-in.service.ts file like this: addUser(newUser) { const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) }; let body = JS ...

React Redux Bundle with Hot Reload Feature

Working on a project written in TypeScript with the React and Redux framework, I'm familiar with webpack and its middleware libraries for hot reloading. My question arises when considering how my TypeScript code is first converted to JSX through gulp ...

angular8StylePreprocessorSettings

I'm currently trying to implement the approach found on this tutorial in order to import scss files through stylePreprocessorOptions in Angular 8. However, I'm encountering an error stating that the file cannot be found. Any suggestions on how to ...

Creating a new function within the moment.js namespace in Typescript

I am attempting to enhance the functionality of the moment.js library by adding a new function that requires a moment() call within its body. Unfortunately, I am struggling to achieve this. Using the latest version of Typescript and moment.js, I have sear ...

Tips for fixing TypeScript compiler error TS2339: Issue with accessing 'errorValue' property in Angular 5 project

Within a component, I have developed a function to manage errors returned from a Rest Service and determine the corresponding error message to display to the user. This method accepts an error object (custom data structure from the service), navigates to e ...