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 mapped type to declare functions

See original GitHub issue

Suggestion

🔍 Search Terms

  • mapped types
  • instance member function
  • instance member property
  • instance method
  • subclass constructor
  • overriding

✅ 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

📃 Motivating Example

In our universe, there are two kinds of animals, dogs and cats.

type Animal = 'dog' | 'cat';

Some people can handle dogs. Some people can handle cats. Some people can even handle both. But if you can’t handle dogs or cats, you’re just not a handler. We can express this generically in TypeScript.

type Handler<A extends Animal> = {[animal in A as `handle${Capitalize<animal>}`]: (animal: animal) => void};

Let’s say you have a dog handler that doesn’t do anything interesting.

class BoringDogHandler implements Handler<'dog'> {
	handleDog(dog : `dog`) {}
}

You could also have someone who can handle both and keeps a record of how many animals handled.

class CountingUniversalAnimalHandler implements Handler<Animal> {
	count = 0;

	handleDog() {this.count++;}
	handleCat() {this.count++;}
}

Some dog handlers are well known for broadcasting to the world whenever they are handling a dog. Since this is such a common thing, we’d like to create a mixin for it.

function NoisyDogHandler<H extends {new (...args: any[]): Handler<'dog'>}>(base: H) {
	return class extends base {
		handleDog(dog : `dog`) {
			console.log("Handling dog");
			return super.handleDog(dog);
		}
	};
}

Looks innocuous enough. But this doesn’t work! Typescript informs us that

Class Handler<"dog"> defines instance member property handleDog, but extended class (Anonymous class) defines it as instance member function. (2425)

If we were allowed to declare that the handle${Capitalize<A>} members were not mere properties, but actually methods, TypeScript would not fear letting us override them. The natural syntax would look like this, which is currently not allowed:

type Handler<A extends Animal> = {[animal in A as `handle${Capitalize<animal>}`](animal: animal): void};

A subtle distinction for a subtle distinction.

Playground Example

💻 Use Cases

What do you want to use this for?

Mix-ins are powerful, and when they work they’re fantastic, but they’re currently somewhat brittle to work with.

This is one of the pieces of the puzzle needed for classes extending constructors of mapped types to override methods (#27689). The other piece needed is to be able to keep the member function status of methods obtained from type indexing (#38496, c.f. #35416, and #46802).

What shortcomings exist with current approaches?

For small enough use cases, you can do the mapping “by hand”:

type Animal = 'dog' | 'cat';

type DogHandler = {handleDog(dog: 'dog'): void};
type CatHandler = {handleCat(cat: 'cat'): void};
type BothHandler = DogHandler & CatHandler;

type Handler = DogHandler | CatHandler;
type HandlerFor<A extends Animal> = ('dog' extends A ? DogHandler : {}) & ('cat' extends A ? CatHandler : {})

Obviously this doesn’t scale well if you create multiple methods with Animal instead of just one, or Animal has many more cases. Less obviously, there isn’t a really satisfactory way to implement a generic method that takes an animal and handler, calls the right handle method, ensures at compile time that the handler can handle the animal, and doesn’t involve casting. See this playground for several attempts.

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:9
  • Comments:6 (1 by maintainers)

github_iconTop GitHub Comments

3reactions
RyanCavanaughcommented, Mar 5, 2022

Based on the use case I’m inclined to just discard the ... defines instance member property ..., but extended class ... defines it as instance member function. error when the base class property is created through a mapped type. Thoughts?

2reactions
rockwalruscommented, Mar 7, 2022

Oh, but if you do discard the error message, make sure it discards it for subclasses as well.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Documentation - Mapped Types - TypeScript
Mapped types build on the syntax for index signatures, which are used to declare the types of properties which have not been declared...
Read more >
Create a function with a specific mapped type as return type
I am trying to create a method that iterates over an object and replaces every key-value pair where the value extends { _id:...
Read more >
Mapped Type Modifiers in TypeScript - Marius Schulz
With TypeScript 2.8, mapped types have gained the ability to add or remove a particular modifier from a property.
Read more >
Mastering TypeScript mapped types - LogRocket Blog
Mapped types are a handy TypeScript feature that allow authors to keep ... a type for an object that can provide string formatting...
Read more >
Chapter 5. Decorators and advanced types - TypeScript Quickly
Mapped types allow you to create new types from existing ones. This is done by applying a transformation function to an existing type....
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 Reddit Thread

No results found

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