New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: Allow local types to be declared in interfaces #9889

Open
danielearwicker opened this Issue Jul 22, 2016 · 12 comments

Comments

Projects
None yet
6 participants
@danielearwicker

danielearwicker commented Jul 22, 2016

I have something like this:

export interface Reducer<State, Types extends Action<string, any>> {

    add<TypeName extends string, Payload>(action: {
        type: TypeName,
        reduce: (state: State, action: Payload) => State
    }): Reducer<State, Types | Action<TypeName, Payload>>;

    readonly cursorType: Cursor<State, Types>;
}

The details aren't that important except to illustrate that a Reducer is immutable, and has an add method that returns another Reducer, but see that the return type has something extra "unioned" into it. By repeated chained calls to add I can build up a big nasty old type that would be ugly to have fully declare by hand. Fortunately type inference takes care of building the type for me, which is great.

Then elsewhere in my code I want to be able to declare something called a "cursor", which needs to have a type that corresponds to the reducer's type. The cursor could be a field in a class so I need to be able to refer to the type so I can declare such a field.

So I want to provide a simple way to declare a const of the type "correct kind of cursor for a given reducer", leveraging the work that the TS compiler already did for me with its type inference.

My slightly hacky approach, as shown above, is to declare a readonly field cursorType. The value of this is at runtime is junk and should not be used! So I need a "here be dragons" comment on it. Its only purpose is to be prefixed with typeof, e.g.:

const R = getReducerSomehow();

class Test {
    constructor(public readonly myCursor: typeof R.cursorType) { }
}

To fill in the cursorType field of a Reducer I have to do this filth:

newReducer.cursorType = {} as Cursor<State, Types>;

So cursorType really should never be used as a value. It doesn't even need to exist as a value. It will cause a runtime error if anyone tries to used it as a cursor. Ugh. But how else can I make this elaborately computed type available conveniently?

I'm wondering if TS could allow:

export interface Reducer<State, Types extends Action<string, any>> {

    add<TypeName extends string, Payload>(action: {
        type: TypeName,
        reduce: (state: State, action: Payload) => State
    }): Reducer<State, Types | Action<TypeName, Payload>>;

    // not currently possible:
    type CursorType = Cursor<State, Types>;
}

i.e. a type alias can be added to an interface. So now my implementation of Reducer no longer has to do anything. No nasty dummy runtime variable hack required.

And my usage example becomes:

const R = getReducerSomehow();

class Test {
    constructor(public readonly myCursor: R.CursorType) { }
}

That is, CursorType is a type that can be referred to as if it was a member of an instance. Similar I guess to:

namespace N {
    export type S = string;
}

const s: N.S = "hi";

In which N is an object at runtime and yet can also be used to find the type S.

@yortus

This comment has been minimized.

Contributor

yortus commented Jul 22, 2016

Seems related to #8358/#8308

@danielearwicker

This comment has been minimized.

danielearwicker commented Jul 22, 2016

@yortus, I wonder if my use case is the same. #8308 initially looks exactly the same (with the same workaround), but was closed in favour of #8358, which seems a lot more vague, and the raiser said he'd also be happy with #2625 which I can't see being related to any of this... So I don't think those other things capture my use case.

Also under #8308 @mhegazy made the interesting suggestion "so namespace expressions, a la class expressions?" I'd like to be super clear about what I'm hoping to wind up with. TS could certainly implement a form of namespace expressions (as I understand them) and yet not address my use case.

A normal named namespace in TS today does have a type:

namespace N {
    export function m() {
        return "test";
    }
}

// structurally like N
interface I {
    m(): string;
}

// Compile-time assertion that N implements I:
const a: I = N;

// Implement typeof N by other means:
const o: typeof N = {
    m() {
        return "hi";
    }
}

All good so far. But if we add a nested type alias to N:

namespace N {
    export function m() {
        return "test";
    }
    type T = typeof m;
}

then we can use type N.T directly, but there is still no such thing as o.T, so typeof N doesn't pick up the type alias N.T. And if the type of a named namespace doesn't include nested type aliases, then why should the type of a namespace expression?

return namespace {
    // regular stuff, some exported, some not

    // and a handy type captured from complicated inference
    export type T = ...
};

By consistency with typeof N, the type of that return value would not have a nested type T either. But that's what I need. So it's possible you could implement namespace expressions and yet not address my use case at all.

If expression namespaces were added, and both named and expression namespaces had a type that could carry nested type aliases, the next thing I wonder is: how would we write down such a type (e.g. for clearer documentation purposes) as an interface? Something like this, I guess:

interface I {
    m(): string;
    type T = string;
}

So we're back to the suggestion above. And if I could do that then I don't specifically need expression namespaces (although I think they'd be awesome for other reasons).

@yortus

This comment has been minimized.

Contributor

yortus commented Jul 23, 2016

Yep all true. I think the commonality is the desire to conveniently access what are otherwise internal types inferred by the compiler, by being able to bind names to these types in more places (in that sense, #6606 is also related).

I think it's a really useful idea to be able to declare a type inside a generic interface. As you point out, the compiler already knows all these types, you are just asking for a convenient way to capture those 'internal' types by binding names to them where they exist.

@isiahmeadows

This comment has been minimized.

Contributor

isiahmeadows commented Sep 3, 2016

👍 to interface-local types (Scala and OCaml both have them, local to classes/objects and modules, respectively). As for them being declared in namespaces, 👎, since TypeScript doesn't exactly use namespaces like how OCaml uses modules. I'd rather the ability to specify them inline in an object and/or class, just like in the interface in your (@danielearwicker) example.

@isiahmeadows

This comment has been minimized.

Contributor

isiahmeadows commented Sep 3, 2016

Oh, and this doesn't actually provide true dependent types alone IIRC. You can't return anonymous interfaces nor manipulate types as values in any sense, even with this proposal.

@danielearwicker

This comment has been minimized.

danielearwicker commented Sep 4, 2016

@isiahmeadows I'm intrigued, what would be examples of things you can't do?

@isiahmeadows

This comment has been minimized.

Contributor

isiahmeadows commented Sep 5, 2016

@danielearwicker There's no equivalent type to the following Scala code:

trait Sigma {
    val foo: Foo
    val bar: foo.Bar // bar's type depends on foo's value to get the type
}

The above code represents a path-dependent type, in which bar depends on the value of foo in that object. So, the above will work like this:

case object FooImpl extends Foo {
  type Bar = Int
}

val bar = new Sigma {
  val foo = FooImpl
  val bar = 1 // Works!
}

In TypeScript, there's no equivalent of this, not even with this proposal. You have most everything else required, but you need the ability to reference the type of a local member without actually resolving the type. The proposed typeof value type won't fix this, though, because that will still evaluate the type of the expression prematurely.


I'll note that not all dependent types can be erased, though. The length of a vector created from a variable-length list is a very good example of this.

@HerringtonDarkholme

This comment has been minimized.

Contributor

HerringtonDarkholme commented Sep 29, 2016

I don't think this is easily done in TypeScript for now. Taking internal types from an interface will introduce many recursions. And most importantly, if two interfaces only differ in exported type, should they counted as the same, structurally? What's the union type of two interfaces with local types? Should local type be covariant like property? Or bivariant like function argument? If an interface occurs at function argument position with local type set to its type parameter, what should be inferred by contextual typing when a plain object is passed? Should function overloading considers local type?

What about referring a local type to a local class? Should two instance to be considered the same?

class A {
  a = new class {}
  type inner = typeof this.a
}
var a1 = (new A).a
a1 = (new A).a // should this compile?
@isiahmeadows

This comment has been minimized.

Contributor

isiahmeadows commented Sep 29, 2016

I'd have to agree with the sentiment. There's too many open questions.
Also, TypeScript kind of needs several additions (like variadic generics
1, to fix several issues with tuples, call, bind, apply, etc.)
before this would become practically useful IMO.

Other problematic constructs include R.curry(f), _.flow(...fs)
(composition of N functions), Object.assign(...objs), etc. These are
functions that you'd have to recursively define the parameter types to
correctly type.

interface ObjectConstructor {
    assign<T, U, ...>(x0: T, x1: U, ...): T & U & ...;
}

declare function flow(f0: (x: A) => B, f1: (x: B) => C, ...);

declare function curry<R, A, B, ..., N>(
  f: (x0: A, x1: B, ..., xn: N) => R
): {
  (x0: A, x1: B, ..., xn: N): R;
  (x0: A, x1: B, ...): (xn: N) => R;
  // other parts of this recursive intersection type
}

There's also an outstanding request for parametric type parameters 2,
critical for libraries like Ramda to address things like Fantasy Land's
interfaces. Here's an example, using that proposal's syntax:

interface Apply<T<~>> {
    "fantasy-land/ap"<A, B>(
        this: T<A>,
        func: T<(x: A) => B>
    ): T<B>;
}

Also, to be fair, it's only just this year that Haskell has gained any
support for dependent types (they have type -> type and value -> value
functions, but type -> value and value -> type functions are still a work
in progress). Very few languages have them, and Scala is the exception, not
the rule, on this one.

On Thu, Sep 29, 2016, 06:43 (´・ω・`) notifications@github.com wrote:

I don't think this is easily done in TypeScript for now. Taking internal
types from an interface will introduce many recursions. And most
importantly, if two interfaces only differ in exported type, should they
counted as the same, structurally? What's the union type of two interfaces
with local types? Should local type be covariant like property? Or
bivariant like function argument? If an interface occurs at function
argument position with local type set to its type parameter, what should be
inferred by contextual typing when a plain object is passed? Should
function overloading considers local type?


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#9889 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AERrBEmc4VAUblspLbuQqXQVLNiIXsCqks5qu5ZLgaJpZM4JSu-2
.

@RyanCavanaugh RyanCavanaugh changed the title from Suggestion: Allow dependent types to be declared in interfaces to Suggestion: Allow local types to be declared in interfaces Oct 24, 2016

@isiahmeadows

This comment has been minimized.

Contributor

isiahmeadows commented Jan 14, 2017

Edit: Fixed an inconsistency.

Just thought of a few libraries that could really use them: React and friends. It'd be much easier to type, say, "components that resolve to a particular element", using a local generic within a generic while still remaining humanly readable. Something like this:

// The outer type carries most of the necessary validation info, and
// could be aliased in React.DOM.
interface Resolution<Name extends string, Element, Attrs> {
    interface DirectChild {
        type: Name;
        props: Attributes;
        // etc.
    }

    // The inner type is much easier to type.
    interface Component<Props, State> {
        state: State;
        props: Props;
        render(props: Props, state: State): ReactComponent<this> | DirectChild;
        // etc.
    }
}

The declaration files would grow a bit, but the user wouldn't see most of the verification boilerplate.

@isiahmeadows

This comment has been minimized.

Contributor

isiahmeadows commented Feb 14, 2017

May I note that allowing inner types to be both overridden and abstract solve the higher kinded type issue entirely? It does not create Turing-completeness, though, and makes it roughly on par with OCaml's module types and inner types.

@alexeygolev

This comment has been minimized.

alexeygolev commented Mar 8, 2017

@isiahmeadows Found this issue while researching into the possibility of doing something like OCaml modules/functors.

NB: OCaml also has a nice feature on the way with modular implicits which is also a powerful concept to consider for TS (and it's compile time and types related so we're safe in terms of TC39 limbo).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment