-
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
Generic values #17574
Comments
As a comparison, flow supports similar feature by wildcard type. // @flow
type Lens<T, U> = {
get(obj: T): U,
set(val: U, obj: T): T
}
// use wildcard
const firstItemInArrayLens: Lens<*[], *> = {
get: arr => arr[0],
set: (val, arr) => [val, ...arr.slice(1)]
}
firstItemInArrayLens.get([10]) // is number
firstItemInArrayLens.get(["Hello"]) // is string |
@HerringtonDarkholme Interesting. If I wanted to express With the proposal above I'd just have |
I guess by default flow will treat wildcard as different type parameters. So two independent varying parameter is the default behavior. The real problem of wildcard is their inter-dependence and bound. For example type like Generic value of course is a better proposal. I just want to list other type checkers' attitude toward similar features. |
I think I found another use for this, let's say I want to have a function that takes in a predicate, and returns the second param wrapped in a box if true, and the third param wrapped in the same box if false. The things you shove in the box can be different types, something like this (with Task from folktale 2 as our box): export type Predicate = () => true | false;
type TestTask = <T>(pred: Predicate, ofTask: T, errTask: any) => Task<T>
// invoke constructors of Task, or some factory function, or perhaps pass those in
const testToTask: <T> TestTask<T> = (p, o ,e) => p() ? of(0) : rejected(e); But instead I need to do const testToTask: <V>(p: Predicate, ofTask: V, errTask: any) => Task<V> =
(p, o, e) => p() ? of(o) : rejected(e); calling it would look like (with folktale2 tasks): let isValidUser = user => () => user != null && user.email != null && user.email.length > 0;
testToTask(isValidUser(user), user, 'Invalid')
.map(user => user.email)
.chain(doSomethingElseAsyncWithEmail)
.run()
.promise() In fact, I'm running into several scenarios where this kind of pattern would be helpful in making some generalized function, but I can't succinctly type it. |
Implementing things like |
I have a need for this as well. Using React, I have a Higher-Order Component that takes a ComponentClass and additional props for that component. I'd like to write an interface that infers its generic from the values of the object, like this: interface IComponentForHOC <P> {
component: React.ComponentClass<P> // <-- inferring P from here
additionalProps: Partial<P>
} And I'd use this interface to enforce that I'm passing the right props for whatever component I pass: class Marker extends React.Component<{
// Props -- the HOC will provide `x`, but `color` needs to be specified.
x: number,
color: string
}> { /* ... */}
// Then Marker is passed to the HOC, which renders it, passing some props
// as well as `additionalProps`.
return <PositionerHOC
objects={[
{
component: Marker,
additionalProps: {color: 'midnightblue'},
},
]}
/> So, that's my use case. I realize I can work around it for now with a function wrapper (below), but that's not ideal. Anyway, I hope to see this feature soon! function createHOCObject <P>(
component: React.Component<P>,
additionalProps: Exclude<P, keyof PropsFromHOC>
): HOCObject<P> {
return { component, additionalProps }
} |
As stated, this wouldn't allow heterogeneous collections? E.g. in I often find myself wanting to use a plugin-registration pattern, something like e.g.: interface Plugin<Config, Props, State extends Storable> {
parseConfig(source: string): Config;
initialState(config: Config): State;
getProps(config: Config, state: State): Props;
render(props: Props): ReactNode;
// ...
}
const plugins: Array<Plugin<???>> = []; // is this a "generic value"? where the code using the type only cares that the result of I've been thinking of this as |
@simonbuchan That's an orthogonal concern. If you wanted to write a polymorphic function that can work with heterogeneous arrays, you wouldn't write it as: const f = <A>(a: A[]) => ... but as: const f = <A extends {}[]>(a: A) => ... That principle would remain unchanged when talking about polymorphic values besides functions that involve heterogeneous arrays. Regarding your proposal of Today I can write: const id: /* for all A */ <A> /* a function from A to A */ (a: A) => A = x => x But I can't simply move that one level out and write: const o: <A> { id: (a: A) => A } = { id: x => x } If the feature were added that allows me to be able to write that type annotation I can obviously move things around and put them in type aliases, i.e. have |
If you were proposing the same thing, then great, but you never said so - all your examples were adding top-level generic parameters on variable declarations - e.g. "generic values" - as Regarding your example, it looks like you're talking about inferring a tuple type. That's not the kind of heterogeneous array I'm talking about, unfortunately! Here's a more specific example, that shows some of the oddness of this: type Item = <Config, Props, State extends Storable> {
plugin: Plugin<Config, Props, State>;
config: Config;
state: State;
};
export function createItems(source: Array<[number, string]>): Item[] {
const items: Item[] = [];
// assume this is standing in for some other magic.
let lastPlugin: Plugin;
for (const [type, configSource] of source) {
// 'plugin' type should be effectively `<C,P,S> Plugin<C,P,S>`
const plugin = plugins[type];
// `const config: <C> C` isn't quite right, as it has to be specifically the `C` declared by `plugin`,
// is this just not possible to write explicitly?
const config = plugin.parseConfig(configSource);
// In particular, it should be a type error to use `lastPlugin.initialState(config)` here,
// as `plugin` and `lastPlugin` might have different types for their `Config`!
const state = plugin.initialState(config);
items.push({ plugin, config, state });
lastPlugin = plugin;
}
return items;
} So far the best answer to this I've found is to refactor the API such that the collections contain homogeneous types, for example: type Plugin = { create(configSource: string): Item; ... };
type Item = { render(): ReactNode; ... };
export function registerPlugin<Config, Props, State extends Storable>(options: {
parseConfig(source: string): Config;
initialState(config: Config): State;
getProps(config: Config, state: State): Props;
render(props: Props): ReactNode;
}) {
plugins.push({
create(configSource) {
const config = parseConfig(configSource);
const state = initialState(config);
// assume there's some other magic to do with dispatching actions and updating state
return {
render() {
return render(getProps(config, state));
};
};
},
// could you ever have another method on a plugin item?
});
};
export function createItems(sources: Array<[number, string]>): Item[] {
return sources.map(([type, source]) => plugins[type].create(source));
} I'm not as big a fan of this style, as it seems harder and more unwieldy to extend with more operations, but that might just be my bias, |
@simonbuchan To clarify, I'm asking for the same feature as Haskell, i.e. the ability to specify quantifiers for type expressions other than functions. I was trying to emphasize this with the corresponding snippet, but I'll try to revise to explain better. Just as in Haskell, the feature needs to be built so that it interacts properly with the rest of the language's machinery for manipulating type expressions. The ability to nest quantified type expressions is known as higher rank polymorphism, and is again something we already have in TS (i.e. you can write |
@RyanCavanaugh Is there some page where we can look up the meanings of issue labels? For example, what is the next step on an "Awaiting More Feedback" issue? |
https://github.com/Microsoft/TypeScript/wiki/FAQ#what-do-the-labels-on-these-issues-mean has a list but doesn't include this label yet. AMF means we'd like to hear from more people who would be helped by this feature, understand their use cases, and possibly contrast with other proposals that might solve the problem in a simpler way (or solve many other problems at once). Concrete next steps (for non-maintainers) would be to identify other problems that would be solved by this feature, or identify library patterns that need this feature to be properly represented. |
I wanted to create a helper library for events, where I could glue events to any class, without the need of modifying the prototype chain. For this reason, it would be awesome if I could do something like this: class Foo extends EventTarget<thing> {
public addEventListener = EventTarget<thing>.addEventListener;
} The only options I'm left with are a) create an instance of |
Question (came up discussing #29613 which overlaps with this somewhat): when you write
How would you prefer to be able to write this, such that you wouldn't have to supply a type parameter? For example, would it be useful to borrow the
(where |
This seems like a pretty fundamental deficiency in the generics system. And in a practical sense, I am bothered by it at least weekly, because properly typing my preferred frontend framework would require this feature. It would be nice to hear some maintainer perspective on the topic. Worrying and frustrating that it is so old with nothing but an "awaiting more feedback" label. Can anything be done to promote this? |
I figure I might as well include my need for generic values (or generic namespaces) here to show that there are actually some folks out here that would like this. I've got a few modules that export a rather similar namespace. // foo.ts
export namespace pubtypes {
type pub_foo = number;
}; // bar.ts
export namespace pubtypes {
type pub_bar = string;
}; If it was only like two or three of these, I'd be fine with just copying 'em around, but it's not. It's like ten. So sometimes whenever I copy them, I make a subtle mistake somewhere along the line and then I get to go back and correct everything else. I'd really like to abstract over them. // pubtypes.ts
export namespace pubtypes<name extends string, pubtype> {
type [key in `pub_${name}`]: pubtype;
}; // foo.ts
export const pubtypes = import("./pubtypes").pubtypes<"foo", number>; Something sorta like that. Awful syntax I'm using but you get the idea. |
Okay, another solid use-case from me. Even though ES6 and higher require JS implementations to do tail-call optimization, the only engine that actually does it is JavaScriptCore in Safari. Some operations (for example, TypeScript's generic type instantiation) require call stacks of arbitrary depth, and rewriting the code manually in order not to use call stack is way too complex. The annoying "type instantiation is excessively deep" message is there for this precise reason. But there is trampolining, technique that allow you to change the code in a minor way, and avoid all the stack problems altogether (those frames are allocated on heap, so no real magic here). For that you need a data structure that holds a function and its arguments, something like deferred function computation. It has an existential type (aka generic value) no matter what. type DeferredCall = <A extends any[], R>{
f: (...args: A) => R,
args: A,
}; Sometimes it's possible to encode existential types with continuations ( |
@RyanCavanaugh Do we need more feedback than 70 comments, and a duplicate with 100 more? |
Any news about this issue? I have a problem which I think is related, let me know if that's not the case : Let's say I want to create an object which represents a change, containing a type Change<T> = {
before: T
after: T
} If I try to do something like this : const change: Change = {
before: 0,
after: 1,
} It doesn't work because the parameter const typed = <T>(change: Change<T>) => change
const change = typed({
before: 0,
after: 1,
}) |
I think that's not applicable here; this issue would be for universally quantified generics (so |
Edit: This happens to work because of a bug in TypeScript's type checker. Please don't use it. For further explanation, continue reading the subsequent posts. Heyyyyy, good news, I think, everybody! Not sure if this is legit, and it's really ugly, Could you take a look? type Lens<out T, out U> = {
get<T_>(obj: T_ extends T ? T_ : never): U;
set<T_, U_>(
val: U_ extends U ? U_ : never,
obj: T_ extends T ? T_ : never
): T;
};
type ArrayLens<out T> = Lens<T[], T>;
const firstItemArrayLens: ArrayLens<never> = {
get: (arr) => arr[0],
set: (val, arr) => [val, ...arr.slice(1)],
};
const intFirstItemArrayLens: ArrayLens<number> = firstItemArrayLens;
intFirstItemArrayLens.get([10]);
const stringFirstItemArrayLens: ArrayLens<string> = firstItemArrayLens;
stringFirstItemArrayLens.get(["Hello"]);
type DeferredCall<out A extends any[], out R> = {
/* f errors with `A_ extends A ? A : never` for some reason */
f: <A_>(...args: A_ extends A ? A_ : never) => R;
args: A;
};
const deferredCallExample: DeferredCall<any[], any> = {
f: () => {},
args: [],
}; Also perhaps ping issue 14466 in case this is a legit solution? Edit: this seems to be a little bit more concise, type Lens<out T, out U> = {
get<T_ extends T = T>(obj: T_): U;
set<T_ extends T = T, U_ extends U = U>(val: U_, obj: T_): T;
};
type ArrayLens<out T> = Lens<T[], T>;
const firstItemArrayLens: ArrayLens<never> = {
get: (arr) => arr[0],
set: (val, arr) => [val, ...arr.slice(1)],
};
const intFirstItemArrayLens: ArrayLens<number> = firstItemArrayLens;
intFirstItemArrayLens.get([10]);
const stringFirstItemArrayLens: ArrayLens<string> = firstItemArrayLens;
stringFirstItemArrayLens.get(["Hello"]);
type DeferredCall<out A extends any[], out R> = {
f: <A_ extends A = A>(...args: A_) => R;
args: A;
};
const deferredCallExample: DeferredCall<any[], any> = {
f: () => {},
args: [],
}; |
Hi @nightlyherb. This is an interesting example, but I think what it illustrates is that interaction between variance checking and quantification constraints is unsound in TypeScript. Here is an example: type Lens<out T, out U> = {
get<T_ extends T = T>(obj: T_): U;
set<T_ extends T = T, U_ extends U = U>(val: U_, obj: T_): T;
};
type MyRecord<A> = { foo: A }
const example: Lens<MyRecord<never>, never> = {
get: ({ foo }) => foo,
set: (v, _) => v // i only promised this works for `never`, so this is fine in principle, if a bit useless
};
// But the variance annotations lie, so i can put my useless lens to nefarious use
const test: Lens<MyRecord<number>, number> = example
const result: MyRecord<number> = test.set(42, { foo: 10 })
console.log(result) // => 42 We can extract a more minimal demonstration of the problem: // It should not be possible to annotate `A` as covariant
type F<out A, out B> = <X extends A = A>(v: X) => B
const a: F<never, never> = v => v
const b: F<unknown, never> = a
const result: never = b(42) // whoops There's actually more than one bug with variance checking that I encountered while playing with your code. These might be worth keeping in mind for anyone playing around with this stuff. For example "methods" don't seem to be checked correctly. If I have: type Lens<in S, out T, in A, out B> = {
get(s: S): B
set(v: A, s: S): T
} Then I can incorrectly annotate the type X<out S> = {
x(s: S): never
}
const a: X<never> = { x: s => s }
const b: X<unknown> = a
const whoops: never = b.x(42) Things are checked "correctlier" if you use As a general note, the "trick" of using |
Edit: I think I was wrong about this whole post. I think the type I suggested isn't covariant. @masaeedu Thank you for the feedback, but I think there might be a misunderstanding (probably on my side) which would hopefully get elucidated and resolved with more discussion. Just so that we're on the same page, I'm aware that That being said I think This is because you actually can put the first "buggy" example you proposed is exactly how I thought people would use this feature. const example: Lens<MyRecord<never>, never> = {
get: ({ foo }) => foo,
set: (v, _) => v // i only promised this works for `never`, so this is fine in principle, if a bit useless
}; The example works for edit: I think this is why the typechecks pass with the "correctlier" version. type Lens<out T, out U> = {
get: <T_ extends T = T>(obj: T_) => U;
set: <T_ extends T = T, U_ extends U = U>(val: U_, obj: T_) => T;
}; Edit: about this example: // It should not be possible to annotate `A` as covariant
type F<out A, out B> = <X extends A = A>(v: X) => B
const a: F<never, never> = v => v // this line seems to be the error
const b: F<unknown, never> = a
const result: never = b(42) // whoops I think the assignment to Other than that all the points you point out with the buggy type inference seems valid. Looking forward for more enlightenment. Thank you! |
@nightlyherb
I haven't thought very carefully about type F1<out A> = <X extends A>(x: X) => never But since we've already started, let's stick with Now, what we're interested in is the question of whether There are a number of ways we can approach this question, but one way is to ask: what are the implications for the overall type system if we consider The consequence I tried to illustrate above is that it becomes possible to inhabit the type F<out A, out B> = <X extends A = A>(v: X) => B
const a: F<never, never> = v => v
const b: F<unknown, never> = a
const result: never = b(42) // whoops To avoid confusion resulting from the type F<out A> = <X extends A = A>(v: X) => never
const a: F<never> = v => v
const b: F<unknown> = a
const result: never = b(42) // whoops Now, why is this bad? Of course there are other ways in TypeScript to inhabit Once you have your hands on an inhabitant of For example I could proceed as follows: const whee: { foo: string } = result
const yay: string = whee.foo + "foo"
console.log(yay) // => "undefinedfoo" In a sense the typechecker sort of just stops working to prevent the sort of problems you'd expect it to prevent. Another way to look at the problem is to interpret the types as propositions and inhabitants as their proofs. In this interpretation the problem boils down to being able to "prove falsehood", since from falsehood, everything follows. Regarding your final edit:
We don't have a Perhaps it's clearer if we write out the example like this: type F<out A> = <X extends A = A>(v: X) => never
const pre: (x: never) => never = v => v
const a: F<never> = pre
const b: F<unknown> = a
const result: never = b(42) // whoops |
Another (probably more persuasive) way to figure out whether something is covariant or contravariant is to try inhabiting the following two types: type F<A> = <X extends A = A>(v: X) => never
const map
: <A, B>(ab: (a: A) => B, fa: F<A>) => F<B>
= (ab, fa) => b => undefined
const contramap
: <A, B>(ba: (b: B) => A, fa: F<A>) => F<B>
= (ba, fa) => b => undefined And see which one you have more success with. |
@masaeedu I think the example of something crashing in runtime explains it the best. type F<out A> = <X extends A = A>(v: X) => void
const a: F<string> = v => console.log(v.toString());
const b: F<string | undefined> = a;
b(undefined); // Cannot read properties of undefined |
Thank you for the helpful response and pointers. I did my homework. Now I'm not so certain about everything, so please take this with a grain of salt.
|
@nightlyherb I think it might be best to continue this discussion in a more relaxed setting (without the anxiety of repeatedly pinging a bunch of people subscribed to this issue). I've created a gist here and responded in the comments. |
Here's another attempt at representing generic values in TypeScript. Sample Codetype Lens<in Ti extends { obj: unknown; val: unknown }> = {
get: <T extends Ti>(obj: T["obj"]) => T["val"];
set: <T extends Ti>(val: T["val"], obj: T["obj"]) => T["obj"];
};
type ArrayLens<in Ui> = Lens<{ obj: Ui[]; val: Ui }>;
const arrayLensExample: ArrayLens<unknown> = {
get: (obj) => obj[0],
set: (val, obj) => [val, ...obj.slice(1)],
};
const numberArrayLensExample: ArrayLens<number> = arrayLensExample;
const typeIsNumber = numberArrayLensExample.get([1, 2, 3]);
const stringArrayLensExample: ArrayLens<string> = arrayLensExample;
const typeIsStringArray = stringArrayLensExample.set("a", ["b", "c", "d"]); Prerequisites for this to workThis assumes that for generic type F defined as
This is ugly! Why do you have to introduce arbitrary contravariance to do this?Well, if this helps you in any way, I intended the types to mean something like... type Lens<in Ti> = <T extends Ti> {
get: <T>(obj: T["obj"]) => T["val"];
set: <T>(val: T["val"], obj: T["obj"]) => T["obj"];
}
type ArrayLens<in Ti> = <T extends Ti> Lens<T[], T> Which are somewhat meaningful generic types. Edit: Important LimitationYou cannot represent something like |
Not having this feature makes it hard to write library code where you want to leverage generics in literal types and allow for relational type constraints around the use of those literals. Examples type Tool<P> = {
name: string;
parameters: P;
};
const runTool = <T extends Tool<any>>(
tool: T, handler: (tool: T["parameters"]) => void
) => {
handler(tool);
};
// it works if we inline it
runTool(
{ name: "string", parameters: { one: "hello", two: false }},
(params) => {
console.log(params.one, params.two);
}
);
// but what if we need to move the literal to it's on declaration? Workarounds:
Typescript Playground Illustrating all these issues When the type system forces developers to actually write worse code, then it seems like an issue worth trying to solve. |
Another use case: decorators. I'm in the middle of implementing a bunch of functions that work as both a function wrapper (i.e. interface FunctionDecorator<F extends (...args: any[]) => any> {
(fn: F): F;
(fn: F, ctx: {kind: "method"}): F;
<D extends {value:F}>(clsOrProto: any, name: string|symbol, desc: D): D;
}
function decorator<F extends (...args: any[]) => any>(decorate: (fn: F) => F): FunctionDecorator<F> {
return function<D extends {value: F}>(
fn: F,
ctxOrName?: string | symbol | {kind:"method"},
desc?: D
) {
if (ctxOrName || desc) return decorateMethod(decorate, fn, ctxOrName, desc);
return decorate(fn);
}
} This works great, right up until you wrap a decorator function that's generic, and then it loses its genericity: const leased = decorator<<T>() =>T>(fn => fn)
// Type 'number' is not assignable to type 'T'. 'T' could be instantiated
// with an arbitrary type which could be unrelated to 'number'.
leased(() => 42) In theory it's still there: the resulting decorator function is typed correctly as taking and returning a function of (As far as I can tell, this is because consts can't be generic, but please feel free to point me to the correct issue if this one isn't it.) |
Hello, TypeScript! Most of what I'm going to write below is already mentioned in this issue thread. However, I do think some points would be helpful for people in need of this feature, so please be gentle even if this seems like repeating past discussions. Summary:
Edit: Maybe "inline" would be a better word than "unhoist"? Oh well, I already dumped a wall of text... Let's have a recap about the problem we are trying to solve. Given a generic type such as: type Lens<T, U> = {
get: (obj: T) => U;
set: (val: U, obj: T) => T;
}; We want to represent something like For instance, the following should typecheck without errors: declare const arrayLens: <V> Lens<V[], V>;
const numberArrayLens: Lens<number[], number> = arrayLens;
const stringArrayLens: Lens<string[], string> = arrayLens;
const neverArrayLens: Lens<never[], never> = arrayLens;
const unknownArrayLens: Lens<unknown[], unknown> = arrayLens; Intuitively, I think of this as the intersection of Anyway, if you see the first post, you can see @masaeedu used the following RHS expression that can be assigned to this hypothetical generic value type: const arraylens: <V> Lens<V[], V> = {
get: (arr) => arr[0],
set: (val, arr) => [val, ...arr.slice(1)],
} I became curious because this already has a representable type in TypeScript, as shown in the first post: type ArrayLens = {
get: <V>(obj: V[]) => V;
set: <V>(val: V, obj: V[]) => V[];
}
const arraylens: ArrayLens = {
get: (arr) => arr[0],
set: (val, arr) => [val, ...arr.slice(1)],
} So I thought it might be assignable to all array-like instantiations of // Type checks without errors
const numberArrayLens: Lens<number[], number> = arraylens;
const stringArrayLens: Lens<string[], string> = arraylens;
const neverArrayLens : Lens<never[], never> = arraylens;
const unknownArrayLens : Lens<unknown[], unknown> = arraylens;
numberArrayLens.get([1, 2, 3]);
numberArrayLens.set(0, [1, 2, 3]);
stringArrayLens.get(["1", "2", "3"]);
stringArrayLens.set("0", ["1", "2", "3"]); So type Lens<T, U> = {
get: (obj: T) => U;
set: (val: U, obj: T) => T;
};
type ArrayLens = {
get: <V>(obj: V[]) => V;
set: <V>(val: V, obj: V[]) => V[];
} It's as if you assign If you think about it this unhoisting process is very natural. Other types behave in interesting ways. If you think about What's interesting is that this seems to work well in more complex cases as well. Let's have our last post from @pjeby as an example: type AnyFunction = (...args: any[]) => any;
interface FunctionDecorator<F extends AnyFunction> {
(fn: F): F;
(fn: F, ctx: {kind: "method"}): F;
<D extends {value: F}>(clsOrProto: any, name: string|symbol, desc: D): D;
}
// The original non-generic type
declare function decorator<F extends AnyFunction>(decorate: (fn: F) => F): FunctionDecorator<F>;
// Generic overload
declare function decorator(decorate: <F>(fn: F) => F): GenericFunctionDecorator;
interface GenericFunctionDecorator {
<F extends AnyFunction>(fn: F): F;
<F extends AnyFunction>(fn: F, ctx: {kind: "method"}): F;
<F extends AnyFunction, D extends {value: F}>(clsOrProto: any, name: string|symbol, desc: D): D;
}
const decoratedGenericFunction: GenericFunctionDecorator = decorator(fn => fn);
const sampleDecoratedFunction: FunctionDecorator<(s: string) => number> = decoratedGenericFunction; This typechecks without errors, so one could use a Thanks for coming to my TED[1] talk. [1] Typescript Enhancement Discussion |
Unfortunately, that seems entirely unrelated to the problem I posted. Try replacing My original code already worked fine with concretely-typed functions (like That is, the inability of TypeScript to express the genericness of a function that is a value, rather than an explicit function declaration. As far as I can tell, the only way to get an actually-generic function with type inference is to explicitly declare all of the overloads, every time you want to make one. |
This error seems to be related to a misunderstanding of TypeScript's type system and is unrelated to this issue. This code gives the same error: const f: <T>() => T = () => 42; I think what you intended with As far as I understand it, Anyway, we can do the following to achieve what you intended (my bad for not including the specific example of yours) playground link |
And yet, if you write As for the specific example, it still doesn't work: the intent is to define decorators with type checking and inference, not to create a generic decorator that can be applied to literally any function. (e.g. leased() requires a function that can be called with zero arguments, which is why the aim is to have a const of type To put it another way: types and functions can be declared with type parameters, but values cannot. (Which is as I understand it the overall point of this issue.) In the specific case of decorators, it means that one must explicitly declare the three overloads for every such function, and can't use a higher-order function to wrap a simpler wrapping function as a multi-purpose decorator. It might be that the issue can be resolved at some other level of abstraction, but at the superficial level of "you can spell it this way, but not this other way", it at least looks like the issue is one of being able to define types and functions as generics, but not values or consts. |
Edit: Oh, I see, did you mean something like this? Note: I just discovered you don't need an overload. |
I don't need an overload for a return value, with your approach, it's true. But it fails if I want to write a decorator that requires arguments and has type constraints on those arguments. I'd like to be able to write: const decorator1 = decorator</* constraints on what kind of function this decorator works with */>(fn => {
/* implementation 1 */
});
const decorator2 = decorator</* constraints on what kind of function this decorator works with */>(fn => {
/* implementation 2 */
});
// .... for any number of decorators, instead of having to write out massive chunks of boilerplate and error-prone overload duplication like: export function pooled<K extends string|symbol,T>(factory: (key: K) => T): (key: K) => T;
export function pooled<K extends string|symbol,T>(factory: (key: K) => T, ctx: {kind: "method"}): (key: K) => T;
export function pooled<K extends string|symbol,T,D extends {value: (key: K) => T}>(
clsOrProto: any, name: string|symbol, desc: D
): D
export function pooled<K extends string|symbol,T,D extends {value: (key: K) => T}>(
factory: (key: K) => T, nameOrCtx?: any, desc?: D
): D | Pooled<K,T> {
/*
several lines of duplicated logic for every single
decorator, instead of being able to use a HOF
*/
/* implementation 1 */
}
// Repeat **all** the same garbage for decorator 2... It might be that my problem is more of a "I want to declare that a function implements an interface" problem than a "generic value" problem, but ISTM that being able to declare a generic value (i.e. one with type parameters) would fix it better, given that declaring a function implements an interface only gets rid of the overloads, not the duplicated implementation bits. (Note that |
Just for the record, I agree with you that it would be nice for TypeScript to support this feature. Anyway, with that said, I think this could be a viable solution, for the subset of this issue, for now: type AnyFunction = (...args: any[]) => any;
interface FunctionDecorator<F extends AnyFunction> {
(fn: F): F;
(fn: F, ctx: {kind: "method"}): F;
<D extends {value: F}>(clsOrProto: any, name: string|symbol, desc: D): D;
}
// The original non-generic type
declare function decorator<F extends AnyFunction>(decorate: (fn: F) => F): FunctionDecorator<F>;
interface FunctionGenericDecorator<BaseF> {
<F extends BaseF>(fn: F): F;
<F extends BaseF>(fn: F, ctx: {kind: "method"}): F;
<F extends BaseF, D extends {value: F}>(clsOrProto: any, name: string|symbol, desc: D): D;
}
// Note the usage of `() => unknown` instead of `() => number`,
// Because this decorator is supposed to accept *all* types of () => T;
// in other words, subtypes of () => unknown
const leased: FunctionGenericDecorator<() => unknown> = decorator(fn => fn);
// correctly infers type with no type errors.
const decoratedFunction = leased(() => 42);
// This is also assignable to instantiations of `FunctionDecorator<F>`
declare function decoratorConsumer<F extends AnyFunction>(decorator: FunctionDecorator<F>): void;
decoratorConsumer(leased); This is flying close to the edge cases of TypeScript's type checker (see #53210) but I guess it's available right now. I'm just hoping TypeScript team doesn't break this in the future. I also think it would be nice if TypeScript could add syntax sugar for similar functionality. (Related: my recent wall of text) |
You already showed that before, and yes it works for 1) parameter-less functions with a generic return value, or else it works for 2) arbitrary functions with no type checking. But since I only have one of the former and none of the latter, it doesn't really address the code duplication issue. I do appreciate your attempts at assistance, though! |
My use case for this feature would be to expose a set of partially-/fully-applied generic types from a library implementation which depends on them. The alternative is to require the consumer of the library to essentially redefine all of the library's individual generic types with the appropriate concrete types. This is not ideal as it increases the burden on the consuming integration and introduces the potential for writing all that out incorrectly. What's bothered me the most about it is that it should not be the library consumer's responsibility to define the library's type concretions—it's the library's responsibility, but the library is limited by the lack of this feature. Until it's officially supported, I've found an ugly workaround to accomplish what I want. The Status QuoI often use a pattern of "initializing" a library to be able to return a set of partially-applied functions based on the initialization parameters. Historically, I've just exported a bunch of generic types from the library and then define their corresponding concretions in the consuming application. Depending on the complexity of those generic types, the type arguments you have to deal with can get messy. /**
* This represents a type we'll want to reference in the library's consuming application.
* There might be a lot of types like this.
*/
export interface Family<Name extends string = string> {
name: Name;
}
/**
* Library initializer with contrived minimal example code to show intent.
*/
export const initialize = <Name extends string>(config: {}) => {
/**
* Some useful library functions.
*/
const greet = (name: Name) => { console.log(`Hi ${name}, the config is: ${config}`) };
/**
* "Export" the library like a module.
*/
return {
greet,
} as const;
}; /**
* Somewhere in the consuming application...
*/
import { initialize, Family as FamilyGeneric } from 'library';
type Name = 'Alice' | 'Bob';
const Library = initialize<Name>({});
/**
* Make concrete types corresponding to each relevant generic type.
* Imagine there are many of these of varying complexity and dependence on each other.
* The type arguments can get wild pretty quickly.
*/
type Family = FamilyGeneric<Name>; The Hack-aroundWith this pattern, I've found a workaround to be able to refer to the types which were effectively made concrete within the initialization function, but which I can't access directly outside of the function. Warning I believe that the code as written below will only work if you can consume the library as TypeScript directly (which I am doing in a monorepo), since it requires that you set both However, if you need export const initialize = <Name extends string>(config: {}) => {
/**
* If the compiler doesn't like this, keep this type out of this function body,
* and then define it again here according to the external definition.
*/
interface Family<N extends Name = Name> {
name: N;
}
const greet = (name: Name) => { console.log(`Hi ${name}, the config is: ${config}`) };
/**
* Group all of the types you want to "export".
* Declare a constant to be that type.
*/
const __TYPES__ = undefined as unknown as {
Family: Family,
};
/**
* The types constant "carries" the concreted generic types out of this function.
*/
return {
greet,
__TYPES__,
} as const;
}; import { initialize } from 'library';
const Library = initialize<'Alice' | 'Bob'>({});
type Types = typeof Library['__TYPES__'];
type Family = Types['Family']; // = initialize<"Alice" | "Bob">.Family<"Alice" | "Bob"> With this, the consuming application no longer needs to be concerned with how to correctly build the library's generic types and maintain those concretions with future updates to the library. At worst, it may just need to define some aliases for the types it cares about. I'm sure this has its own set of limitations around what can be done with the resulting types, but thought I'd share this in case anyone else finds it useful. |
TypeScript supports generic type/interface/class declarations. These features serve as an analog of "type constructors" in other type systems: they allow us to declare a type that is parameterized over some number of type arguments.
TypeScript also supports generic functions. These serve as limited form of parametric polymorphism: they allow us to declare types whose inhabitants are parametrized over some number of type arguments. Unlike in certain other type systems however, these types can only be function types.
So for example while we can declare:
we cannot declare (for whatever reason):
One result of this limitation is that the type constructor and polymorphism features interact poorly in TypeScript. The problem applies even to function types: if we abstract a sophisticated function type into a type constructor, we can no longer universally quantify away its type parameters to get a generic function type. This is illustrated below:
Another problem is that it is often useful to quantify over types other than bare functions, and TypeScript prohibits this. As an example, this prevents us from modeling polymorphic lenses:
In this case, a workaround is to break down the type into functions and move all the polymorphic quantification there, since functions are the only values that are allowed to be polymorphic in TypeScript:
By contrast, in Haskell you'd simply declare an inhabitant of a polymorphic type:
firstItemInArrayLens :: forall a. Lens [a] a
, similarly to the pseudocode declarationconst firstItemInArrayLens: <A> Lens<A[], A>
:In some sense TypeScript has even less of a problem doing this than Haskell, because Haskell has concerns like runtime specialization; it must turn every polymorphic expression into an actual function which receives type arguments.
TypeScript just needs to worry about assignability; at runtime everything is duck typed and "polymorphic" anyway. A more polymorphic term (or a term for which a sufficiently polymorphic type can be inferred) can be assigned to a reference of a less polymorphic type.
Today, a value of type
<A> (x: A) => A
is assignable where a(x: number) => number
is expected, and in turn the expressionx => x
is assignable where a<A> (x: A) => A
is expected. Why not generalize this so an<A> Foo<A>
is assignable wherever aFoo<string>
is expected, and it is possible to write:const anythingFooer: <A> Foo<A> = { /* implement Foo polymorphically */ }
?The text was updated successfully, but these errors were encountered: