composition interfaces
Right now we can extend an interface with another interface. This syntax is similar to the class inheritance syntax where you extend classes with other classes.
export interface Animal {
shout: () => void;
}
export interface Human extends Animal {
think: () => void;
}
What would be awesome is the ability to use composition to describe interfaces.
This would allow us to explain the relationships between interfaces in terms of behaviours rather than inheritance trees. This is model that languages like Go employ
MPJ gives a much better explanation of the use cases than I can
https://www.youtube.com/watch?v=wfMtDGfHWpA
export interface Shouter {
shout: () => void
}
export interface Thinker {
think: () => void
}
export interface Animal {
...Shouter
}
export type Human = {
...Shouter
...Thinker
}
My suggestion meets these guidelines:
How is that any different than the first example? It should have some sort of appreciable benefit to warrant additional syntax in my opinion. Interfaces are allowed by be extended from multiple other interfaces as well, plus other advanced type operators allow all sorts of other strange ways of combining types.
Also, why did you ignore the issue template?
I apologize, I have updated my post to include the template and a more comprehensive example
You can do that already without adding new syntax. The only caveat is that you have to use types instead of interfaces:
export interface Shouter {
shout: () => void
}
export interface Thinker {
think: () => void
}
export type Animal = Shouter;
export type Human = Shouter & Thinker;
export type SpecialHuman = Shouter & Thinker & {
poop: () => void;
};
TIL I am an Omit<SpecialHuman, "think">.
Being serious now: it's not clear what differences you have in mind. Is it the idea that extending a type requires assignment compatibility and requires you to resolve conflicts when extending multiple types?
If the aversion is to the compatibility checks (which I think are valuable), then do intersection types as above model what you have in mind?
I don't understand the example in OPs original post, because here it is using current TypeScript. No intersection, just extend. So we have extend, intersections — and also declaration merging falls into this general area. IMO OP fails to make a case for something not covered by those features.
@alshdavid hasn't actually verified whether they have something else in mind, so let's not pile up here. If the existing features solve their problem, then we can close the issue.
The existing features solve their problem
The existing features do solve the problem. Union types do facilitate this technically.
I've been thinking a lot about what I am trying to solve with this and I think I am directing my attention at the wrong language feature.
I'll close this but just to continue the discussion I'd love to get some thoughts from TypeScript contributors.
Using factory functions and leveraging the type system is challenging in TypeScript. Are factory functions a pattern that is generally discouraged with TS? Are we intending to move the language toward a less "functional" direction?
I worked with factory functions and composition in TypeScript for quite a while. There have been some improvements in the language since that time, but really the challenge was more that ECMAScript really hasn't tackled traits and composition as part of the language specification. It was part of the original Harmony specification, which is where the class syntax came from, but traits were too much for TC39 to swallow in one go, so they put all that on hold, getting the MVP through. There has been off and on again talking about more syntactical support, but I don't believe currently there is really anyone championing it.
The vast majority of the TypeScript compiler is factory functions, so they are certainly supported. We eventually gave up on factory functions when the TypeScript team relaxed some constraints in the compiler and made traits/mixins realistic, and ever since then, it has just been easier to embrace the class constructor. Using a mixin class would look something like this:
type Constructor<T> = new (...args: any[]) => T;
class Animal {
shout() {};
}
interface ThinkerMixin {
think(): void;
}
function ThinkerMixin<T extends Constructor<Animal>>(Base: T): T & Constructor<ThinkerMixin> {
return class Thinker extends Base {
think() {}
};
}
const Thinker = ThinkerMixin(Animal);
const myThinker = new Thinker();
myThinker.shout();
myThinker.think();
For the most part the types just flow through, the only thing you have to really deal with is that you have to provide an interface for what each mixin function would mixin, but once you have done that, the types just flow through.
Thanks for the insight. Do you have any thoughts around the implementation of object declaration in the form of Structs - such as in Go (Golang)?
In Typescript we would write:
type Person = {
name: string
walk: (this: Person, steps: number) => void
}
// "this" is the parent object
function walk(this: Person, steps: number) {
console.log(this.name, 'walked', steps, 'steps')
}
function newPerson (name: string): Person {
return {
name,
walk
}
}
const person = newPerson('marvin')
person.walk(8)
Which would be expressed like this in Go
type Person struct {
Name string
}
// p is the parent object
func (p Person) Walk(steps string) {
fmt.Println(p.name + " walked " + steps + "steps")
}
func newPerson(name string) Person {
return Person{
Name: name,
}
}
const person = newPerson("marvin")
person.Walk(8)
Differences are that the struct itself is a type, rather than being a factory function that accompanies a type, which is nice because you don't have to maintain both the type and the factory function.
The receiver is defined behind the function name rather than being a this as a first parameter.
The receiver parameter is an arbitrary variable name, and doesn't have to be this.
The struct implicitly adds methods to it's type based on what functions take it as a receiver, rather than requiring explicit declaration in it's type definition
Thoughts?
This behaviour, of having to maintain a seperate interface, is finally what led us away from factory functions and just embrace class. It really isn't TypeScript as much as JavaScript/ECMAScript is a prototypical language, of which class is arguably bad/confusing syntactic sugar for a constructor function which instantiates a prototype.
The only "trick" to make it more self sustaining would be something like this, but this could be really awkward:
const PersonPrototype = {
name: "" as string,
walk
};
type Person = typeof PersonPrototype;
function walk(this: Person, steps: number) {
console.log(this.name, 'walked', steps, 'steps');
}
function createPerson(name: string): Person {
const person = Object.create(PersonPrototype) as Person;
Object.assign(person, { name });
return person;
}
const person = createPerson('marvin');
person.walk(8);
Most helpful comment
TIL I am an
Omit<SpecialHuman, "think">.