question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Allow inferring a type alias for derived return types

See original GitHub issue

Suggestion

🔍 Search Terms

function inferred return type alias

✅ Viability Checklist

My suggestion meets these guidelines:

  • This wouldn’t be a breaking change in existing TypeScript/JavaScript code
  • This wouldn’t change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn’t a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript’s Design Goals.

⭐ Suggestion

It would be extremely convenient if a function were able to declare a type alias for its return type inline. A possible syntax for this could be:

const createCounter = (): infer Counter => {
  const result = {
    count: 0,
    increment: () => ++result.count,
  };
  return result;
};

This would create a type alias automatically, allowing usage like:

import { Counter, createCounter } from './counter';

📃 Motivating Example

Currently, a class declaration automatically emits a type for the resulting constructor function. However, if an object is created with a standard function, this is not possible. The inferred return type of functions can be aliased like:

// aliased type:
// type Counter = {
//   get: () => number;
//   increment: () => number;
// }
export type Counter = ReturnType<typeof createCounter>;

const createCounter = () => {
  const result = {
    count: 0,
    increment: () => ++result.count,
  };
  return result;
};

This is an powerful pattern, as it allows the implementation to be the source of reference for the type, rather than the other way around. However, while the aliased type is derived from the function, the function itself does not return the aliased type.

// type is:
// const counter: {
//   get: () => number;
//   increment: () => number;
// }
// NOT Counter
const counter = createCounter();

In order to use the derived type, there are 2 options, both of which are clumsy:

  • Put the onus on the caller to explicitly use the type:
import { Counter, createCounter } from './counter';

const counter: Counter = createCounter();
  • Create a superfluous higher order function:
export type Counter = ReturnType<typeof createCounterInternal>;

export const createCounter = (): Counter => createCounterInternal();

const createCounterInternal = () => {
  // counter impl
};

💻 Use Cases

Any function that returns a complex object could benefit from this.

Issue Analytics

  • State:open
  • Created a year ago
  • Reactions:31
  • Comments:6

github_iconTop GitHub Comments

1reaction
ehaynes99commented, Jun 10, 2022

Admittedly, I don’t know how much complexity this is under the hood, but as a language feature, it would be particularly nice if it could be used to alias the internals of a generic type, promises being the most obvious case:

const createCounter = async (): Promise<infer Counter> => {
  const result = {
    count: 0,
    increment: () => ++result.count,
  };
  return result;
};
0reactions
ehaynes99commented, Aug 27, 2022

I want to discuss where this infer Type statement can be applied and what should be its semantic.

The semantics of the infer keyword already exist with conditional types:

type Result<T> = T extends Promise<infer R> ? R : T

While there is no formal definition in the docs, one might be:

infer <Alias>:

Given an unnamed type that can be statically inferred, assign the reified type
to the supplied alias

The semantics would remain unchanged. This merely alters the language syntax to allow it in a different place.

For me this infer Type feature should be very limited in what it does.

It already is. This change would merely add one additional allowable location which would be function return types.

I think the mental model should be:

The mental model should be the same as a class. Stripping away the syntactic sugar of ES6 classes, a class is really just a constructor function. TypeScript provides additional syntactic sugar the make it define:

  • A value, which is the the constructor function
  • A type alias, which is the instance type returned by the constructor function

Declaring a class gives access to both. Classes are quite limited, however, and this would allow similar convenience to “creator” functions, which are far more flexible.

I think infer should only introduce alias in the scope (here the module’s scope). If you want to export it then you need to export it manually (export { FunResult }). This code is basically equivalent to:

That would completely defeat the purpose. Callers would be unable to use the alias, only the anonymous inferred type, so exporting the function without exporting the type alias would be exactly the same as omitting the type.

When you declare a class, the value and the type always share a lexical scope. Imagine if they didn’t, and instead required an explicit export type ClassName

export class ExampleClass {
  constructor(
    public first: string,
    public second: number,
  )
}

// we forgot to export this commented line
// export type ExampleClass
// some-other-file.ts
import { ExampleClass } from './example'

// type of `example` would be an anonymous `{ first: string; second: number }`
const example = new ExampleClass('one', 2)

// and you couldn't explicitly refer to it, as this would be an error:
// Cannot find name 'ExampleClass'.ts (2304)
const example: ExampleClass = new ExampleClass('one', 2)

  1. Variable declaration Should we allow using infer Type in variable declaration?

Definitely not. It’s conceptually inverted. Inference on the “left hand” of an assignment statement is inferring the type of the variable, not describing the type of the expression’s result. The “right hand” expression is evaluated first and always produces a result of a specific, unambiguous type. Even if it’s anonymous, any, or unknown, it’s still a value with a concrete type. If you happen to assign it to a variable, the variable’s type is a reflection of that value’s type, not a definition for it.

let t1: FunType[] = someFun();
type FunType = {
  foo: string;
  bar: number;
};

someFun “owns” the type here. Any variable assigned to its result should be that type. The alias produces confusion, not clarity. The only reason you would want an alias on the consumer side is if someFun is external code with a missing or incorrect type. Even then, you’re better off wrapping the function in another that defines a better type:

type FunType = {
  foo: string;
  bar: number;
}

const typedFun = (): FunType[] => {
  return someFun()
}

Then assigning variables to that “more correct” type:

// inferred type of `t1` is `FunType[]`
const t1 = typedFun();
Read more comments on GitHub >

github_iconTop Results From Across the Web

Documentation - Advanced Types - TypeScript
Type aliases create a new name for a type. Type aliases are sometimes similar to interfaces, but can name primitives, unions, tuples, and...
Read more >
typing — Support for type hints — Python 3.11.1 documentation
At runtime, the statement Derived = NewType('Derived', Base) will make Derived a callable that immediately returns whatever parameter you pass it. That means ......
Read more >
TypeScript: Is it possible to get the return type of a generic ...
There is a proposal to allow using typeof with arbitrary expressions to allow things like getting the return type of a generic functions...
Read more >
Kinds of types - mypy 0.991 documentation
A value with the Any type is dynamically typed. Mypy doesn't know anything about the possible runtime types of such value. Any operations...
Read more >
Opaque Type Aliases - Scala 3 - EPFL
Opaque types aliases provide type abstraction without any overhead. Example: object MyMath: opaque type Logarithm = Double object Logarithm: // These are ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found