Skip to content

Support simple dependent types based on conditional types or inedexed types #43876

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

Closed
5 tasks done
garyo opened this issue Apr 29, 2021 · 3 comments
Closed
5 tasks done
Labels
Suggestion An idea for TypeScript Too Complex An issue which adding support for may be too complex for the value it adds

Comments

@garyo
Copy link

garyo commented Apr 29, 2021

Suggestion

πŸ” Search Terms

dependent types, computed types, generics, conditional types, indexed types

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

I'd like to be able to declare a variable within a function whose type depends on the type of an argument to a generic function.

πŸ“ƒ Motivating Example

In this example, repr in foo needs to be of type Repr1 when foo's arg is of type Comp1, and similarly Repr2 for Comp2. So repr's type is the dependent type I'd like to allow.

Currently this doesn't compile because the assignment to repr is incompatible with its type, and the call to bar is "no overload matches this call".

type Comp1 = {type: "comp1", id: string, desc: string}
type Comp2 = {type: "comp2", id: string, name: string}

type Repr1 = {comp: Comp1, id: string, desc?: string}
type Repr2 = {comp: Comp2, id: string, name?: string}

function bar(c: Comp1, r: Repr1): void
function bar(c: Comp2, r: Repr2): void
function bar(c: Comp1|Comp2, r: Repr1|Repr2) {
  console.dir({c, r})
}

function foo<T extends Comp1 | Comp2>(comp: T) {
  let repr: T extends Comp1 ? Repr1 : T extends Comp2 ? Repr2 : never
  // do other things with compatible comp and repr here -- as an example:
  repr = {id: comp.id, comp} // error 1 here
  bar(comp, repr) // error 2 here
}

πŸ’» Use Cases

This would simplify some of my code that currently has to use type guards to implement each case separately, where the only variation is the type of the repr variable, like this version (which works today):

function isComp1(c: any): c is Comp1 {return c.type === 'comp1'}
function isComp2(c: any): c is Comp2 {return c.type === 'comp2'}

function foo<T extends Comp1 | Comp2>(comp: T) {
  if (isComp1(comp)) {
    let repr: Repr1
    repr = {id: comp.id, comp}
    bar(comp, repr)
  } else if (isComp2(comp) {
    let repr: Repr2
    repr = {id: comp.id, comp}
    bar(comp, repr)
  }
}
@MartinJohns
Copy link
Contributor

Somewhat related: #27808 and #40827.

And just pointing out that T isn't always either Comp1 or Comp2. The type can be Comp1 | Comp2 as well.

@garyo
Copy link
Author

garyo commented Apr 29, 2021

And just pointing out that T isn't always either Comp1 or Comp2. The type can be Comp1 | Comp2 as well.

Hmm, interesting. I guess the extends oneof syntax in #27808 would help here, because that is certainly my intent. Every instance of a comp should have a definite/concrete type, not Comp1|Comp2. I just tried adding overloads for foo to only accept a Comp1 or a Comp2, but that didn't change anything.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Too Complex An issue which adding support for may be too complex for the value it adds labels Apr 29, 2021
@RyanCavanaugh
Copy link
Member

Paring this back to the things not covered by other issues, I don't think this plausibly fits in our complexity budget. This isn't even really a request about generics per se; an equal complaint is that the nongeneric form

function foo(comp: Comp1 | Comp2) {
  let repr = { id: comp.id, comp };
  bar(comp, repr);
}

doesn't typecheck either.

What you'd need to make this pass is one of two things:

  • A combinatorially-explosive check iterating the possible inhabitants of comp
  • A higher-order reasoning system that tracked that the value of repr was (and still is) derived from some other value; this is described in other issues elsewhere as a "same utterance" problem where there is code that is only showable to be correct if we track the actual values themselves rather than their types

The latter system there, expanded to generics, is massively complicated and would only really be sound in cases where the code itself is sort of overcomplex for no identifiable reason. In this example, bar taking both c and r makes it much harder for TS to reason about. An example that isn't overcomplicated (e.g. legitimately needs two separate parameters originating from different values) is extremely unlikely to be sound in the presence of values constructed from a generic like shown here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Suggestion An idea for TypeScript Too Complex An issue which adding support for may be too complex for the value it adds
Projects
None yet
Development

No branches or pull requests

3 participants