Skip to content

Allow inferring generic function types "as const" #38968

Closed
@mmkal

Description

@mmkal

Search Terms

const generic inference nested

Suggestion

Let generic function signature specify that type parameters should be inferred as const

Use Cases

Allow writing library functions that specify how they want their generic parameters to be inferred. Right now, this is only possible with generics like <T extends string>(value: T) => ..., which doesn't cover objects - only literals.

Examples

Let's say you have a basic runtime type-matching function:

const getMatcher = <T>(sample: T) => <U>(value: U): value is T & U => {
  // implementation details not important - just a very basic deep-equals
  if (sample && typeof sample === 'object') {
    return Object.entries(sample).every(([k, v]) => k in value && getMatcher(v)((value as any)[k]))
  }
  return sample === (value as any)
}

If I try to use this to make a string matcher, it doesn't keep track of literal types:

const fooMatcher = getMatcher('foo') // T is inferred as `string`, not literal `"foo"`

const s = Math.random() < 0.5 ? 'foo' : 'bar'

if (fooMatcher(s)) {
  const foo: 'foo' = s // Error: Type '"foo" | "bar"' is not assignable to type '"foo"'.
}

This makes sense as default behaviour, since 'foo' was passed in as a string. And it can be fixed at the call site with getMatch('foo' as const). But this only works for typescript programs, and if we're writing a library, javascript users of that library won't get useful type inference. And it's not really intuitive. Even typescript users of the library will need to know somehow that this trick is available.

It could also be fixed by changing the function signature:

const getMatcher = <T extends string>(sample: T) => ...

But getMatcher is now limited to only work with strings. This doesn't cover the case of deeply nested objects:

const commentMatcher = getMatcher({
  type: 'comment',
  payload: {sender: 'abc'},
})

As far as I know, there's no way to tweak the extends constraint for T which makes this keep track of the literal type: 'comment' and sender: 'abc' values. So this feature request is to allow us to effectively ask the compiler to add as const to any inferred type parameters:

const getMatcher = <T as const>(sample: T) => <U>(value: U): value is T & U => {
  ...
}

(syntax is just an idea which reuses the as const pattern in the generic typedef)

If we could do this, it'd make it easy to use the matcher to write useful type guards:

type Request =
  | {type: 'comment'; payload: {sender: string; issueId: number; body: string}}
  | {type: 'issue'; payload: {sender: string; id: number; title: string; body: string}}

const commentMatcher = getMatcher({
  type: 'comment',
  payload: {sender: 'abc'},
})

const handleRequest = (request: Request) => {
  if (commentMatcher(request)) {
    console.log('parent: ' + request.payload.issueId) // request would be correctly inferred as a comment, because `type: 'comment'` was inferred as a literal
  }
}

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, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions