-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Comments
@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 return namespace {
// regular stuff, some exported, some not
// and a handy type captured from complicated inference
export type T = ...
}; By consistency with 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). |
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 |
👍 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. |
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. |
@isiahmeadows I'm intrigued, what would be examples of things you can't do? |
@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 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 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. |
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? |
I'd have to agree with the sentiment. There's too many open questions. Other problematic constructs include 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, 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 On Thu, Sep 29, 2016, 06:43 (´・ω・`) [email protected] wrote:
|
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. |
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. |
@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). |
I have something like this:
The details aren't that important except to illustrate that a
Reducer
is immutable, and has anadd
method that returns anotherReducer
, but see that the return type has something extra "unioned" into it. By repeated chained calls toadd
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 withtypeof
, e.g.:To fill in the
cursorType
field of aReducer
I have to do this filth: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:
i.e. a
type
alias can be added to an interface. So now my implementation ofReducer
no longer has to do anything. No nasty dummy runtime variable hack required.And my usage example becomes:
That is,
CursorType
is a type that can be referred to as if it was a member of an instance. Similar I guess to:In which
N
is an object at runtime and yet can also be used to find the typeS
.The text was updated successfully, but these errors were encountered: