microsoft/TypeScript

Generic values

masaeedu opened this issue · 72 comments

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:

type Id = <T>(x : T) => T

const test : Id = x => x

we cannot declare (for whatever reason):

type TwoIds = <T> { id1: (x: T) => T, id2: (x: T) => T }

const test : TwoIds = { id1: x => x, id2: x => x }

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:

// This works
type Id = <T>(x : T) => T

// This should also work
type IdT<T> = (x: T) => T
type Id = <T> IdT<T> // but is impossible to express

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:

type Lens<T, U> = {
    get(obj: T): U
    set(val: U, obj: T): T
}

// no way to express the following type signature
const firstItemInArrayLens: <A> Lens<A[], A> = {
    get: arr => arr[0],
    set: (val, arr) => [val, ...arr.slice(1)]
}

firstItemInArrayLens.get([10])      // Should be number
firstItemInArrayLens.get(["Hello"]) // Should be string

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:

type ArrayIndexLens = {
    get<A>(obj: A[]): A
    set<A>(val: A, obj: A[]): A[]
}

const firstItemInArrayLens: ArrayIndexLens = { ... }

By contrast, in Haskell you'd simply declare an inhabitant of a polymorphic type: firstItemInArrayLens :: forall a. Lens [a] a, similarly to the pseudocode declaration const firstItemInArrayLens: <A> Lens<A[], A>:

{-# LANGUAGE ExplicitForAll #-}

data Lens s a = Lens { get :: s -> a, set :: a -> s -> s }

firstItemInArrayLens :: forall a. Lens [a] a
firstItemInArrayLens = Lens { get = _, set = _ }

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 expression x => x is assignable where a <A> (x: A) => A is expected. Why not generalize this so an <A> Foo<A> is assignable wherever a Foo<string> is expected, and it is possible to write: const anythingFooer: <A> Foo<A> = { /* implement Foo polymorphically */ }?

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 Lens<A, B>, where A and B are allowed to vary independently (i.e. Lens<number, string[]> is allowed), how would flow express that with wildcards?

With the proposal above I'd just have <A, B> Lens<A, B>.

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 ADependsB<A, B extends A> is hard to express in wildcard. (I guess, flow type sadly does not have official doc for wildcard type.)

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 Maybe is also inconvenient without generic values; you want the types of the constructors to be Just :: a -> Maybe a and Nothing :: Maybe a. That is, Nothing should be a value is acceptable wherever a Maybe String is wanted, as well as where a Maybe Number is wanted (but not, for example, where a String is wanted).

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 const arrayLenses: <A> Array<Lens<A[], A>> , all the items have the same A? Syntactically you could allow Array<<A> Lens<A[], A>>, but I'm not sure there's a good way to deal with that (e.g. the type of arrayLenses[0] would have to have a fresh A in each evaluation somehow).

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 plugin.parseConfig() can be passed to plugin.initialState() for the same item. For now I've been using <any>, but that allows passing config as state, etc., in the using code, which sucks.

I've been thinking of this as type Plugin = <Config, Props, State> { ... }, e.g. allow adding generic parameters to any type expression, which I think also covers the original suggestion, is that generalization problematic?

@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 type Plugin = <Config, Props, State> { ... }, I'm not really sure how this is different from what's already been proposed. The original proposal is to make it possible to introduce existential quantifiers at the head of arbitrary type expressions instead of just allowing it at the head of function types.

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 type ObjWithIdProp = <A> { id: (a: A) => A } and then const o: ObjWithIdProp = { id: x => x }, just as you could write type Id = <A>(a: A) => A and then const id: Id = x => x. This is the same as the original proposal, not a generalization of it.

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 unique symbol shows, variable declarations can have more assumptions made about them that may make a difference to the feasibility here. If you are asking to add generic parameters to any type expression (including nested type expressions), then great, we want the same feature!

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 const f: <A>(g: <B>(b: B) => B) => A). Any correctly implemented generalization of type quantification from functions to arbitrary values should give you that for free.

@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.

cshaa commented

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 EventTarget every time I want to implement it, b) create a factory for addEventListener or c) give up strict typing. I was really disappointed when I spent whole day designing a good, type-safe library only to find out it's that ugly to use.

Question (came up discussing #29613 which overlaps with this somewhat): when you write

let id1 = undefined as Id1<any> // forced to supply type parameter here

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 infer keyword for this purpose, like

let id1 = undefined as Id1<infer A>;

(where A is understood to be a special kind of declaration, not a reference)?

@MattiasMartens I'd want to be able to write it as:

const id1 = undefined as (<A> Id1<A>)

If some separator is needed between the quantifiers and type expression to assist parsing (e.g. <A>. Id1<A>), that's fine too.

The specific syntax actually doesn't matter that much to me so long as it is possible to conceptually map the behavior of the TypeScript code straightforwardly to the behavior in languages where this feature is already well established (e.g. id1 = (undefined :: forall a. Id1 a) in Haskell).

I have a use case that I believe would be covered by this feature. Forgive me if I've misunderstood, but I've run into two or three situations over the past few months where I've wanted to do something similar to the following (contrived) example:

const KEYWORD_ALIASES = {
  'foo-bar': 'Foo Barrington',
  'usps': 'United States Postal Service',
  'scooby_doo': 'Scooby-Doo: Where Are You!',
  'stunt.props': 'Underwhelming Stunt Props Shop',
} as const;

type KeywordAliases = typeof KEYWORD_ALIASES;
type Keyword = keyof KeywordAliases;
type AliasForKeyword<T extends Keyword> = KeywordAliases[T];

// I want the generic, `K`, to be inferred from the properties of the
// assigned object, such that `keyword` and `alias` are both ensured
// to be their "correct" string literal types, without supplying a
// redundant type parameter
type Foo<K extends Keyword> = {
  keyword: K;
  alias: AliasForKeyword<K>;
  // ...
};

// `K` would be inferred here as 'usps', not 'foo-bar'|'usps'|'scooby_doo'|'stunt.props'
// `AliasForKeyword<K>` is 'United States Postal Service' if `K` is 'usps'
// `AliasForKeyword<K>` is 'Underwhelming Stunt Props Shop' if `K` is 'stunt.props'
const foo: Foo = {
  keyword: 'usps',
  alias: 'Underwhelming Stunt Props Shop', // Should error; correct type is the literal 'United States Postal Service',
  // ...
};

While that would be really nice, that's more partial inference of types: in Flow syntax that would be const foo: Foo<*> = { ... }, where the * means "figure it out, but it still has a definite type at compile time", meaning the same thing as if you had typed const foo: Foo<'usps'> = { ... }.

This is about having non-function values having a type that is still generic, like function types.

In this situation, that would be:

type Foo = <K extends Keyword>{
  keyword: K;
  alias: AliasForKeyword<K>;
  // ...
};

let foo: Foo; // Foo is a type, but is still generic (not a generic instantiation)
foo = { keyword: "foo-bar", alias: "Foo Barrington", /* ... */ }; // ok
foo = { keyword: "usps", alias: "United States Postal Service", /* ... */ }; // ok
foo = { keyword: "usps", alias: "Underwhelming Stunt Props Shop", /* ... */ }; // error

// but:
// foo.keyword is 'foo-bar'|'usps'|'scooby_doo'|'stunt.props'
if (foo.keyword == "usps") {
  // foo.alias is "United States Postal Service"
}

In this case, it flattens to being a different way to make unions, but it can used in more general ways.

In particular, I'm interested in it being a way to represent useful heterogeneous arrays, where each item has a type relation to itself but not other items - this comes up a lot if you're building generic plumbing, e.g. plugins or components.

@simonbuchan That's a much better example of what I was going for, and matches my [real-world] use case more accurately.

sgoll commented

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.

@RyanCavanaugh Our use case is similar to the one described by @kjleitz above: we'd like to have a base schema type that others can pass to us and we must derive generic types from that—for React components, for function calls etc.

For instance, the "generic values" proposal would allow us to get the generic props type of a React handler component:

// Proposed "generic value" syntax:
type Props<S> = <K extends keyof S>{
  key: K,
  onData(data: S[K]): void;
}

Here, S would be a schema definition and we'd expect the following (generic) props type to work:

interface Schema {
  foo: number;
  bar: string;
}

const p: Props<Schema> = {
  key: 'foo',
  onData(data) {}
}

Our workaround consisted of variations of ReturnType<…> of some generic function that has the desired type as return value but this does not cover all cases because we'd always end up with unintended type unions:

type Props<S> = ReturnType<<K extends keyof S>() => {
  key: K,
  onData(data: S[K]): void;
}>

const p: Props<Schema> = {
  key: 'foo',
  // `data` should have type `number` but ends up being `string | number`:
  onData(data) {}
}

My use case is the decorator pattern in oop or generally speaking middleware patterns with generic functions that have different return types based on the input. Since its atm not possible to get the ReturnType of a generic function, I could solve this with a generic value - mapping the function input to the correct return type.

My "solution" for the moment is to completely avoid generic middleware ^^

Lenses, and many other functional paradigms in which functions are just values, is my use case.

I have a very similar representation as the original description. Although in the end, function types are involved, I think a proper solution would give a better representation.

type FooMutator<T, I, O> = (data: T, change: I): Result<T, O>

// The following is just a specialization of FooMutator, polymorphic over the
// content type of an array
// But, as presented in OP, only the third syntax works, the type of FooMutator
// must be repeated here
// Even worse! Since `I` and `O` depend on the content type in the array, we have
// to write a really specialized type to represent that, that is basically only used
// to give a type to the following constant, and nowhere else
type GenericInsertMutator = <V>(data: V[], change: V): Result<V[], never>
const insertMutator: GenericInsertMutator = ...; // Works and gives correct types
// In case we know the array content type, we can simply assign to the "target"
// type in terms of `FooMutator`
const stringInsertMutator: FooMutator<String[], String, never> = insertMutator;

// Would like to be able to write something like
// const insertMutator: <V> FooMutator<V[], V, never>

Here's a potential use case: generic React components:

import * as React from 'react';

type Props<T> = { t: T };

// Ideally we could somehow re-use the `React.FC` function type:
// declare const MyComponent: <T> FC<Props<T>>;

// But we can't, so we have to write out the whole of the `FC` type, inline in the function definition:
const MyComponent = <T>(props: Props<T>): ReactElement<any> | null =>
  <div>Hello, World!</div>

Another use case: given this generic React HOC type,

import { ComponentType } from 'react';

type HOC<P, P2> = (
    ComposedComponent: ComponentType<P>,
) => ComponentType<P2>;

… when we use it to define our HOC, we want the resulting HOC type to also be generic:

// Ideally we could somehow re-use the `HOC` function type:
// declare const myHoc: <P> HOC<P>

// But we can't, so we have to write out the whole of the `HOC` type, inline in the function definition:
declare const myHoc: <P>(ComposedComponent: ComponentType<P>) => ComponentType<P>;

(Originally filed in #32973.)

declare const MyComponent: <T> FC<Props<T>>;

Isn't this just:

type MyComponent<T> = FC<Props<T>>;

@fatcerberus Nope, it's a value, not a type. It's also not the same as:

type MyComponentType<T> = FC<Props<T>>;
declare const MyComponent: MyComponentType;

which is currently illegal, and making it declare a generic value would conflict with generic parameter defaults (e.g. if it were type MyComponentType<T = whatever> = ...).

My use case is also generic React components, and drilling generics through HOC's like this:

type Props<P extends {}> = { ... } & P;
type ViewProps<P extends {}> = Props<P> & { ... };

const enhance = <P extends {}>(
  component: React.ComponentType<ViewProps<P>>
): React.ComponentType<Props<P>> =>
  compose(
    ...myHOCs
  )(component);

const MyViewComponent = <P extends {}>(props: ViewProps<P>) => (
  <div>
    ...
  </div>
);

const MyComponent = enhance(MyViewComponent); // No longer generic, and infers any

I have a use case that's React, but not specific to react and more about arrays, I think similar examples where mentioned but for additional examples

I'm building a Table Component

interface Table<Row> {
  rows: Row[]
  columns: Column<Row>[]
}

The Column of my table has a property prop that is a keyof the row, such that the value of the row for that prop would be passed to the custom render function of my column.

interface Column<Row, Prop extends keyof Row> {
  prop: Prop
  render: React.FC<{ value: Row[Prop], column: Column<Row, Prop>, row: Row }> 
}

this way the user of my table when writing the custom render function would get the correct type hint

interface Employee {
  id: number
  name: string
  address: { street: string, building: 1 }
}

const table: Table<Employee>  = {
  rows: [{...}]
  columns: [
    {
      prop: 'id',
      render(id) {
        // should be 'number' but is 'number | string | { street: string, building: 1 }'
        // also currently render(id: number) throws a type error ('number' does not match 'number | string | { street: string, building: 1 }' for some reason which forces me to use any
      }
    },
  },
  ]
}

I think this is the same case for heterogeneous arrays @simonbuchan was referring too in response to @kjleitz

To reinforce @OliverJAsh 's comments, I too just want to create generic React FunctionComponents. Imagine a straightforward component which takes properties based around a generic type, much like a List component might. It's easy to create such a component as a class and have proper typings:

export interface FooProps<T> {
	data: T[];
	onBoop: (item: T) => void;
}

export class FooClass<T> extends Component<FooProps<T>> {
	render() {
		// Here data is T[] and onBoop is (item: T) => void
		const { data, onBoop } = this.props;
		return (
			// ...
		);
	}
}

But I'm encouraged to use pure function components rather than classes. When I try to create one, explicitly specifying its type, I cannot:

// This is invalid - Cannot find name 'T'. ts(2304)
export const FooFC1: FunctionComponent<FooProps<T>> = ({ data, onBoop }) => (
	// ...
);

Like @OliverJAsh said, I can sort of work around this by explicitly specifying types for the props and return values:

export const FooFC2 = <T extends unknown>({ data, onBoop }: FooProps<T>): JSX.Element | null => (
	// ...
);

But this is annoying and technically inaccurate. The FunctionComponent interface definition contains more than just the arguments and return value of the function: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/ca1d51aa09e7fc8ff3b955d96c439f91fff22433/types/react/index.d.ts#L515-L521

👍 this issue, it comes up whenever you have objects that contain related polymorphic functions. The lack of this feature makes lenses super awkward to work with in Typescript which is a shame as they make working with immutable data so much easier. You can't write polymorphic lenses that can be re-used, you always have a specify concrete types. The lack of partial application of generic parameters also means you end up with an awkward calling syntax.

import { Lens } from 'monocle-ts'

interface Foo {
   bar: number
}

// type :: Lens<Foo, number>
const bar_ = Lens.fromProp<Foo>()('bar')

When in reality the proper type of the Lens is Lens<A extends { bar: unknown }, A['bar']> because it should be able to work with any type containing a bar

My use case is actions, mutations and getters from vuex store. We have lots of components that share the same basic state, and then we have something like

const baseMutations: MutationTree<BaseComponent<object, null, null, object, object>> = {
  initComponent(state, data) {
    state.data = data;
    // ...
  },
  // ...
}

Notice the <object, null, null, object, object>. When instantiated properly, it contains types of various parts of components state, which is specific to each Vue component. However, none of that is relevant to the base mutations.

Then we need to create the state of a concrete Vue component

const incrementerStore: Module<BaseComponent<{ a: number}, ...>, RootStore> = {
  state() {
    return { a: 0 };
  },
  mutations: {
    ...baseMutations, // Problem: BaseComponent<{ a: number}, ...> is not assignable to BaseComponent<object, ...>
    incrementA(state) {
      a += 1;
    },
  },
}

This calls for generic baseMutations variable. The only other solution I see is to make baseMutations a generic function and needlessly return a new object for every Vue component.

@mik-jozef
Do you have a more concrete example?

Maybe a Playground?

I've created this example which is copy-pastable to TypeScript Playground - complete and no dependencies. I've tried to strip as much irrelevant code away as possible while still showing how it works in the real project, but I have no idea if I succeeded, so please let me know.

/// Section: Vue and Vuex types, copied & slightly modified from:
/// https://github.com/vuejs/vuex/blob/dev/types/index.d.ts

interface MutationTree<S> {
    [key: string]: Mutation<S>;
}

type Mutation<S> = (state: S, payload?: any) => any;

export interface Module<S, R> {
    namespaced?: boolean;
    state?: S | (() => S);
    mutations?: MutationTree<S>;
}


/// Section: Base functionality. This code is shared among many of Vuex modules.

// State of a Vuex module.
interface BaseComponentState<Filter extends object|null> {
    filter: Filter,
    // There are other properties here in the real code, each with own generic param.
}

// GENERIC VALUES ALERT: Comments indicate where a generic type should be used.
const baseMutations: /* <Filter extends object|null> */ MutationTree<BaseComponentState<object|null /* Filter */>> = {
    initComponent(state, data: { filter: object|null /* Filter */ }) {
        state.filter = data.filter;

        state.filter = null; // PROBLEM: This must be error - Filter can be {}, see PcList below.
        state.filter = {}; // PROBLEM: This must be error - Filter can be null, see Dummy below.
        // There is no specific type that BaseComponentState can be instantiated with.
        // Any attempt to produce an error here will also produce error in one
        // of the specific examples below. Try `object`, or `null` to see yourself.
    },
};

/// Section: A specific store example - a list of PCs that
/// can be filtered according to search criteria.

type PcType = 'new' | 'used' | 'either' | null;

const pcListStore: Module<BaseComponentState<{ type: PcType }>, any> = {
    namespaced: true,
    state: {
        filter: {
            type: null as PcType,
            /* Let's not clutter potential error messages.
            priceMin: null as number|null,
            priceMax: null as number | null,
            ramMin: null as number | null,
            ramMax: null as number | null,
            */
        },
    },
    mutations: baseMutations,
};


/// Section: A specific store example - one that doesn't use filter.

const dummyStore: Module<BaseComponentState<null>, any> = {
    namespaced: true,
    state: {
        filter: null,
    },
    mutations: baseMutations,
};

I think I discovered a very simple example that shows why generic types are needed:

Note: EDITED.

function foo<X, R extends Record<string, (arg: X) => X>>(r: R) {
    return r;
 }

// `typeof x` should be `<X> { a: (s: X) => X; }`.
const x = foo({ a: (s) => s });

// `typeof t` should be `3`.
const t = x.a(3);

What's wrong with this?

function foo<R extends Record<string, <X>(arg: X) => X>>(r: R) {
    return r;
 }

// `typeof x` should be `<X> Record<string, (arg: X) => X>`.
const x = foo({ a: (s) => s });

// `typeof t` should be `3`.
const t = x.a(3);

Playground

Ah, so my example was too simple. I still believe typescript should be able to handle it as is, but here is an improvement:

interface MutationTree<S> {
    [key: string]: (state: S, payload?: any) => any;
}

function foo<X, M extends MutationTree<X>>(m: M) {
    return m;
 }

// `typeof x` should be `<X> { a: (s: X) => X; }`, a subtype of `<X> MutationTree<X>`.
const x = foo({ a: (s) => s });

// `typeof t` should be `3`.
const t = x.a(3);

.
.

MutationTree cannot be rewritten to

interface MutationTree {
    [key: string]: <S> (state: S, payload?: any) => any;
}

(because it is defined in a library, and) for the same reason Set cannot be rewritten to

interface Set {
  has: <T> (arg: T) => boolean;
  add: <T> (arg: T) => Set;
}

. The generic parameter of MutationTree is part of what it means to be a mutation tree and its definition is correct (just like with Set).

Isn't that just arbitrarily enforcing constraints that don't make sense?
If you cannot rewrite MutationTree, just create another interface and call it MutationTree2 or something.

//The thing that must-not-be-overwritten
interface MutationTree<S> {
    [key: string]: (state: S, payload?: any) => any;
}

//Just create a new thing
interface MutationTree2 {
    [key: string]: <S>(state: S, payload?: any) => S;
}

function foo<M extends MutationTree2>(m: M) {
    return m;
 }

// `typeof x` should be `<X> { a: (s: X) => X; }`, a subtype of `<X> MutationTree<X>`.
const x = foo({ a: (s) => s });

const stringMutationTree: MutationTree<string> = x; //OK
const numberMutationTree: MutationTree<number> = x; //OK
const point2dMutationTree: MutationTree<{ x : number, y : number }> = x; //OK

Playground

... Which I guess is the point of the issue. People don't want to rewrite types.

MutationTree2 is not the same as MutationTree<S> in your example because the S of each value of the interface is independently quantified instead of all being forced to be the same S as in MutationTree<S>. As I said above, in Typescript, generic types may only be introduced by functions, so any object with two or more generic functions that should be constrained to the same generic value is not possible to express as a value. Which is why re-usable Lenses aren't expressible.

As I said above, in Typescript, generic types may only be introduced by functions, so any object with two or more generic functions that should be constrained to the same generic value is not possible to express as a value

I wondered what that would look like:

declare const foo: <T>{
  bar<U extends T>(x: U): void,
  baz<U extends T>(x: U): void,
};

... Which I guess is the point of the issue. People don't want to rewrite types.

  1. Do you believe that it's right for a programming language to force people to have duplicate types? Is copy-paste programming the official TypeScript's policy here? Because MutationTree2 is not something that would appear anywhere in normal code. It's a hack meant to work around the language's limitations. What about single source of truth? And the type MutationTree2 is not even correct, as @DylanRJohnston said.

  2. Even if none of the above applied and MutationTree2 was a valid workaround, it still changes nothing about the fact that there are programs which TypeScript is unable to type correctly, and lack of generic values is the reason why. That my simple example (the one with Record<string, (arg: X) => X>>) might be editable to something that works is not relevant to that, because in the example without any modifications, type of t should be 3.

In the MutationTree example, quantifying per value is a stronger requirement than quantifying over the entire object.

So, MutationTree2 is a subtype of MutationTree. I wasn't trying to make them "the same". I was trying to get a subtype that would be assignable to MutationTree. And I did just that.


I'm not against this feature request. I 👍 'd this issue a long time ago. I do have use cases for it. The MutationTree example just didn't seem like a good use case. So, I was curious about it, and wanted to see if I was missing something

React functions like memo or forwardRef also use interfaces/function types that have type parameters annotated at the head of interface instead of at the call signatures, so they have to be instantiated when calling above functions.

Quintessence: You always get a concrete instead of generic component out of memo and forwardRef. This is similar to and partially uses the React.FC type mentioned in #17574 (comment) .

Possibly related: The free type parameters could then be propagated via (some extended form?) of higher order function type inference. Currently that is not possible, as stated here. Hope, that makes sense.

I just came across this issue as well with the following use case: Enforcing the shape of functions, such as conversion functions similar to the From and Into traits from Rust. That is, given a type such as...

type From<T, U> = (value: T) => U // From a `T`, construct a `U`

...I'd like to be able to declare a function such as:

const arrayFromIterator: <T> From<Iterator<T>, Array<T>> = (iterator) => // Convert iterator to array

The benefit of this is that the type helps enforce the shape of the function and, in addition to the name of course, clearly communicates its intent.

Another use case is function composition of variadic-curried functions—e.g. _.flow(foo, bar, ... )(baz)—with a generic function as the first argument of the composition function, and allowing type arguments to be passed and correctly inferred.

// Can't be inferred if used as the first argument to a curried function
const foo = <TBaz>(baz: TBaz): Foo => { ... };
// Can be generic—or not—because only the first function passed to "_.flow" is unable to infer the correct type(s)
const bar = (foo): Bar => { ... };
// Currently infers "TBaz" as "any"/"unknown" (depending on TypeScript version); i.e. "LodashFlow<unknown, Foo, Bar>"
const fooBar = _.flow(foo, bar);
// TypeScript therefore accepts any/all values to be given as the argument for "fooBar"
fooBar(x);

Assuming it can be parsed, I propose allowing generic parameters to be declared; 1) following value declarations, e.g. const fooBar<TFoo>; and 2) following value expressions, e.g. fooBar<TFoo>.

In the example below, TFoo is an argument to the parameter TBaz—for foo—as a value (not as a callable signature), since it's not invoked:

// Declare type parameter "TFoo" for the value "fooBar"
// Then, allow passing it to "foo" for the original type parameter "TBaz"
const fooBar<TFoo> = _.flow(foo<TFoo>, bar);
// Then allow "fooBar" to be invoked with "Baz" passed as "TFoo"—through to "TBaz"
fooBar<Baz>(baz);
// Or even assigned to another value—without being invoked
const fooBarBaz = fooBar<Baz>;
// To allow later invocation that is correctly typed
// OK; since typeof "fooBar" === "(baz: Baz) => Bar"
fooBarBaz(baz);

This also allows the alternative:

// Pass the type "Baz" directly to the parameter "TBaz"
const fooBar = _.flow(foo<Baz>, bar);
// Error: type "X" is not assignable to type "Baz"
fooBar(x);

Here's another use case to add to the pile. I'm trying to create a declaration file for a javascript library that has a type rule I am finding hard (impossible?) to express with typescript, due to this limitation. (I could be mistaken of course. I'm rather new to TS so feel free to correct me!)

The library raises events which users are expected to define handlers for. Events provide payloads of varying types, so the type of an event handler needs to be parameterized on the payload type it can accept.

Handlers can be either a function that take the payload as argument:

type HandlerFunc<P> = <S>(state:S, payload:P) => S

(the library will call this handler with the payload of the event it is assigned to, when the event is raised)

or a "bound" handler like for example: [someHandlerFn, 42]. Assigning a bound handler like that to an event will also cause someHandlerFn to be called, but the payload will be 42 instead of whatever payload the event offers. In other words we also have:

type BoundHandler = [HandlerFunc<P>, P]

Here, the event's payload type doesn't matter. Only that the type of payload given in the tuple is something the handler function can deal with. But the syntax above is illegal of course. If it were possible to use the same kind of type inference and unification (is that the correct term?) that occurs in generic functions – but in tuples – I would easily be able to define the type of a handler as:

type HandlerFunc<P> = <S>(state: S, payload: P) => S
type BoundHandler = <P>[HandlerFunc<P>, P]
type Handler<P> = HandlerFunc<P> | BoundHandler

It seems I might be able to work around this limitation by using deeply nested extends clauses in generic-function declarations of the library's functions. But I'm having a very tough go of it and this feature would really help.

This looks like a duplicate or at least somehow interchangable with #14466?

I came across the same issue while trying to create an interface with properties that all have the same property type twice. As in, all of the entries have two properties that use a common type, but all of the entries use a different one. In this case, this is for a list of event listeners with filters, that use the event listener's arguments as their input:

type EventFilter = <T> {
    registerListener: (listener: (...args: T) => any) => void,
    filters: {
        name: string
        test: (...args: T) => boolean
    }[]
}

interface EventList {
    [key: string]: EventFilter // I want this to infer T uniquely for each value
}

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 (<R>(cb: (a: A) => R) => R), because you can set that <R> in front of a function. It's not the case when trampolines are implemented. Unfortunately generic values do exist, and currently there is no way to type them in TS.

@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 before value and an after value. Like so :

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 T needs to be explicitly set.
Instead I have to use a dummy function which works but seems unnecessary :

const typed = <T>(change: Change<T>) => change
const change = typed({
  before: 0,
  after: 1,
})
jcalz commented

I think that's not applicable here; this issue would be for universally quantified generics (so Change would have to act as a Change<T> for every possible T, which it couldn't be). Presumably your use case is for existentially quantified generics (so Change would act as a Change<T> for some T you don't care about), in which case you want #14466. Or maybe you want the compiler to infer T? If so then you want #32794.

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!
I think I partially implemented this in typescript.

Not sure if this is legit, and it's really ugly,
but at least it seems to work.

Could you take a look?
(Edit: you might want to look at the other form below)

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,
although I'm not sure if the two forms are equivalent.

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 S and A parameters as out and TypeScript appears not to care. We can boil this (unrelated) issue with variance checking down to the following example:

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 { x: (s: S) => never } instead of { x(s: S): never }, but there remains the earlier mentioned problem of interaction between quantified constraints and variance annotations.


As a general note, the "trick" of using F<never> to stand for forall x. F<x> is a very useful one, but it only works if F is purely covariant in its type parameter (dually we can use F<unknown> if it is purely contravariant). In this situation the typechecker facilitates a lie about F being covariant ("simple"/non-type-changing lenses are unfortunately fundamentally invariant in both their type parameters), and everything that follows is inconsistent as a result.

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 F<X, Y> = (x: X) => Y is covariant to Y and contravariant to X.

That being said I think F1<X, Y> = <X_ extends X>(x: X_) => Y or F2<X, Y> = <X_>(x: X_ extends X ? X_ : never) => Y are entirely different expressions, and I actually do think it's covariant w.r.t X (and Y).

This is because you actually can put F1<sub-X, Y> where F1<super-X, Y> is needed.

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 never, thus it can work for any type T. This is assignable because the way you presented the RHS can be applied to any Lens<MyRecord<T>, T>. If you replace a more specific value on the RHS I do see that it doesn't work.

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 a is where the error happens, because <X extends A>(v: X) => X is assigned to <X extends A>(v: X) => B.

Other than that all the points you point out with the buggy type inference seems valid.

Looking forward for more enlightenment. Thank you!

@nightlyherb

Just so that we're on the same page, I'm aware that F<X, Y> = (x: X) => Y is covariant to Y and contravariant to X.

That being said I think F1<X, Y> = <X_ extends X>(x: X_) => Y or F2<X, Y> = <X_>(x: X_ extends X ? X_ : never) => Y are entirely different expressions, and I actually do think it's covariant w.r.t X (and Y).

I haven't thought very carefully about F2, but let's focus our attention on F1. FWIW it was probably a mistake on my part to include the second type parameter to F1 for the purposes of this discussion, and I should have restricted myself to the simpler type:

type F1<out A> = <X extends A>(x: X) => never

But since we've already started, let's stick with F1 as it is.

Now, what we're interested in is the question of whether F1 "is covariant" in its first type parameter.

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 F1 to be covariant in its first type parameter?

The consequence I tried to illustrate above is that it becomes possible to inhabit the never type. Here is that demonstration again:

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 B parameter, here is an analogous example with a single type parameter:

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 never, but these inhabitants typically represent programs whose meaning is a runtime blowup or error or infinite stuck evaluation. Modulo "programs that fail to evaluate", being able to inhabit the bottom/uninhabited/never type is typically an undesirable feature of a type system called inconsistency.

Once you have your hands on an inhabitant of never, the subtyping relation allows you to pretend it is an inhabitant of any type at all, and the ensuing behavior of the program is unspecified.

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:

I think the assignment to a is where the error happens, because <X extends A>(v: X) => X is assigned to <X extends A>(v: X) => B.

We don't have a <X extends A>(v: X) => X in the example. What we do have is (x => x) : <X>(x: X) => X, which can be suitably instantiated to yield a (x: never) => never, which is assignable where a <X extends never = never>(x: X) => never is expected.

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 think I need to take my time to sort this out, thanks a lot and I'll come back with a better understanding, hopefully!


I did my homework. Now I'm not so certain about everything, so please take this with a grain of salt.

  1. Typescript's generic functions seem to act like they're contravariant over the type parameter's type bound. I'm not entirely sure though.

    Playground Link

    // Assignment succeeds with F<unknown> -> F<string>
    // Assignment fails with F<string> -> F<unknown>
    
    const a1: <T extends unknown>(x: T) => T = (x) => x;
    const b1: <U extends string>(y: U) => U = a1;
    const c1: typeof a1 = b1; // error
    
    const a2: <T extends unknown>(x: T) => string = String;
    const b2: <U extends string>(x: U) => string = a2;
    const c2: typeof a2 = b2; // error
    
    declare const a3: <T extends unknown>() => T;
    const b3: <U extends string>() => U = a3;
    const c3: typeof a3 = b3; // error

    @masaeedu This seems like the {co,contra}variance of the types is independent to whether you can do a {co,contra}map on it. May I ask, did I mess up again, or is this another unsoundness, or is this the way it is? Anyhow thank you so much for the helpful discussion! ❤️

@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 Code

type 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"]);

Playground Link

Prerequisites for this to work

This assumes that for generic type F defined as F<in Ti> = <T extends Ti>(generic function having T as its type parameter) is contravariant to Ti. i.e. if Tsub is a subtype of Tsuper, F<Tsuper> is a subtype of F<Tsub>.

  • This is the behavior I observed when I manually instantiated F with specific types. You can see that in this post I made a while before.

  • Illustrative Example
    if F<Ti> = <T extends Ti>(x: T) => T,

    • string is a subtype of unknown
    • it seems logical that F<unknown> is a subtype of F<string> because
      • F<string> can be assigned to:
        • (x: string) => string
        • (x: StringSubType) => StringSubType
        • (x: never) => never
        • etc...
      • F<unknown> can be assigned to all of that plus even more types such as:
        • (x: number) => number
        • (x: unknown) => unknown
        • etc...
  • I couldn't find unsoundness with this assumption, but I'm terrible at detecting unsoundness so we'll have to see about that...

  • Another reason you might want to wait before using this seriously. As of Mar 19 2023, TypeScript can only check (and report errors of) specific instances of F. When you use the generic type F it doesn't check its variance at all and allows all variances over Ti. See my blunder above or #53210.

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 Limitation

You cannot represent something like <T> Lens<(x: T) => void, T> because if you try to represent it as type SpecialLens<T> = Lens<(x: T) => void, T> then SpecialLens cannot be contravariant over T. Currently typescript doesn't error on type SpecialLens<in T> = ... so I guess it's another reason we shouldn't use this construct until #53210 is fixed.

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:

  1. Explicitly write out type parameters.: this can often be more work than writing the code itself.

  2. Leave the object untyped: this doesn't provide the developer with type info/constraints until they go to use it.

  3. Use Lens: This is a cool trick, but pollutes the type definitions with complexity that makes it not worth it (IMO).

  4. Use an "identity" function: hurts DX as now you have to hunt down an additional function just to make your generic types. Also, I imagine this could have a non-trivial impact on performance in some cases.

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.

pjeby commented

Another use case: decorators. I'm in the middle of implementing a bunch of functions that work as both a function wrapper (i.e. wrap(f)) and a method decorator (i.e. @wrap), supporting both legacy and TC39 decorator protocols. I'd like to be able to express this by just wrapping each plain F => F decorator in a function that returns a function that supports all three required overloads (function, function + context, class/name/descriptor):

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 <T>() => T, but any specific function you pass in gets an error saying that what it returns isn't <T>, because T could be an arbitrary type. And there doesn't seem to be a way to say, "no, I want you to infer T", except by hand-writing out all three overloads (plus a butt-ugly implementation signature) for every function, which is both tedious and error-prone if you have a lot of these.

(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:

  1. You can "unhoist" the type parameters of the hypothetical "generic value" type to get a type that is already representable in current TypeScript. (As shown in the "workarounds" for the first post)

  2. This type is assignable to every instantiated expression of the original generic type as far as I can test.

  3. To resolve this issue it seems sufficient to simply add syntax sugar for this "unhoisting" process.

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 <V> Lens<V[], V>, which should be assignable to Lens<V[], V> for every type V. (For those that have types that work for some V but not necessarily for every type, visit #14466.)

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 Lens<V[], V> for all types V. (And existential types #14466 as the union for all types V.)

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 Lens<T, U>, and indeed it is:

// 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 ArrayLens seems like a valid representation for <V> Lens<V[], V>.
Let's compare the two types:

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 T <- V[] and U <- V to the type arguments and "unhoist" the angle brackets for the type params.

If you think about it this unhoisting process is very natural.
If ArrayLens = <V> Lens<V[], V>, then ArrayLens["get"] should be able to receive a V[] and return a V, for every type V. This is effectively a <V>(obj: V[]) => V. Same goes for ArrayLens["set"].

Other types behave in interesting ways. If you think about <T> { co: T; contra: (x: T) => number; }, covariance/contravariance over T forces the type to resolve to some trivial type (which is unusable most of the time). In this case, { co: never; contra: (x: unknown) => number; }

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 FunctionDecorator<F> and use it for both generic and non-generic functions.

Thanks for coming to my TED[1] talk.

[1] Typescript Enhancement Discussion

Unfortunately, that seems entirely unrelated to the problem I posted. Try replacing (s: string) => number with <T>() =>T in your example, and then try invoking sampleDecoratedFunction(() => 42) (or any other constant value), and you'll get the same "Type 'number' is not assignable to type 'T'. 'T' could be instantiated with an arbitrary type which could be unrelated to 'number'" error as in my example.

My original code already worked fine with concretely-typed functions (like string => number), but neither it nor your example work with generic function types (e.g. <T>() => T), which was what I was posting about.

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.

Unfortunately, that seems entirely unrelated to the problem I posted. Try replacing (s: string) => number with <T>() =>T in your example, and then try invoking sampleDecoratedFunction(() => 42) (or any other constant value), and you'll get the same "Type 'number' is not assignable to type 'T'. 'T' could be instantiated with an arbitrary type which could be unrelated to 'number'" error as in my example.

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 <T>()=>T is related to #14466.

As far as I understand it, <T>() => T is a type that can be thought of as "for all types T, the intersection of every () => T". This should be compatible with () => never.

Anyway, we can do the following to achieve what you intended (my bad for not including the specific example of yours) playground link

As far as I understand it, <T>() => T is a type that can be thought of as "for all types T, the intersection of every () => T". This should be compatible with () => never.

And yet, if you write function x<T>(): T {} it's interpreted as generic. That is, you can declare a function generic, but not a value.

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 FunctionDecorator<F>, where F is a type parameter, rather than GenericFunctionDecorator, which is effectively FunctionDecorator<AnyFunction>.)

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 pooled as defined above accepts variants such as string => number, symbol => SomeClass, etc., and returns functions that are of the same type, rather than accepting any (string | symbol) => unknown function and returning a function of that type. Also some decorators will have other type constraints, such as that a certain parameter must be a WeakKey, etc. The point is to have a fully generic HOF for turning F =>F wrappers into functions with two extra overloads that support the TC39 and legacy/experimental protocols for method decorators for the corresponding function type.)

Just for the record, I agree with you that it would be nice for TypeScript to support this feature.
I do think that how to work around using currently available TypeScript is valuable information for some people as well (especially because this issue has been remaining open for 7 years), which is why I wrote my recent post.

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 Quo

I 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-around

With 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 declaration and declarationMap to false. Without that, you get the error ts(4025): "Exported variable has or is using private name."

However, if you need declaration and declarationMap enabled, I believe you can still use this approach if you just keep the generic types defined outside of the initializer function and reference those generic types within the function to make the same concrete types as shown below. This results in more type parameter boilerplate, but the compiler shouldn't be upset about the internal type, and the extra code mess is still contained within the library instead of all the consuming applications.

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.