Skip to content
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

Narrowing fails with union of functions #51639

Closed
ElianCordoba opened this issue Nov 24, 2022 · 6 comments
Closed

Narrowing fails with union of functions #51639

ElianCordoba opened this issue Nov 24, 2022 · 6 comments
Labels
Not a Defect This behavior is one of several equally-correct options

Comments

@ElianCordoba
Copy link

Bug Report

πŸ”Ž Search Terms

function union, function union, multiple signatures

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried. It's not fixed in nightly

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

type SimpleFn = (text: string) => string 
type WithArgsFn = (text: string, ...options: string[]) => () => string

interface FnMapper {
  [key: string]: SimpleFn | WithArgsFn;
}

const mapper: FnMapper = {
  foo: (text) => '', 
//      ^^^ Error, text is any, should be string
}

πŸ™ Actual behavior

Parameter text implicitly has an any type.

πŸ™‚ Expected behavior

No error plus text should be of type string

@Andarist
Copy link
Contributor

I can't be sure right now if the root issue is the same but this issue feels related: #48663

@ahejlsberg
Copy link
Member

We only infer contextual parameter types from a union of contextual function types when the contextual function types have identical parameter lists, so this is effectively a design limitation. We could possibly do better by inferring the widest possible parameter types in such situations.

@RyanCavanaugh RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Dec 1, 2022
@RyanCavanaugh
Copy link
Member

It's very very rarely useful to contextually type by a union of functions -- the only cases where it really helps are basically places where you could have used the subtype instead, as is the case here (using WithArgsFn instead).

@RyanCavanaugh RyanCavanaugh closed this as not planned Won't fix, can't repro, duplicate, stale Dec 1, 2022
@fatcerberus
Copy link

@RyanCavanaugh For the record, manual subtype reduction isn't possible here because one has a return type of () => string while the other just string. BUT this actually highlights an issue with the code as written: the signatures overlap. WithArgsFn can legally be called without any options, in which case the union with SimpleFn suggests it's intended to return a string. But if a given caller is only passing text there's no way for the compiler to tell the two signatures apart.

@ackvf
Copy link

ackvf commented Dec 8, 2023

Similar.

Here I am writing a dictionary of "checks" (isAddress, isMatching, etc.) for form validation. The developer is supposed to follow the required form, which was supposed to be enforced by the satisfies operator. Unfortunately the type intellisense breaks with this union. It's such a shame, this would have been a perfect use case. Is it at all possible to achieve something similar with current state of TS?

stack overflow
playground

type Valish = (value: string) => boolean
type CurryValish = (...args: number[]) => Valish

const check = {
  vin: (value) => true,            // βœ— any, should be string
  cin: (curry) => (value) => true, // βœ— any any, should be number string
} satisfies {
  [key: string]: 
    | Valish
    | CurryValish
}

Some other examples of what the object is supposed to hold.
It's basically a function that checks the value of an input, e.g. min(10)(input.value) or notEmpty(input.value)

const c = (regex) => new RegExp(regex)
const r = (characters) => new RegExp(`^[${characters}]+$`)
let minLength, isMatching

export const check = {
  isTrue: (value) => value == true,

  /* numbers */

  min: (min: number) => (value: Numberish) => Number(value) >= min,
  max: (max: number) => (value: Numberish) => Number(value) <= max,
  clamp: (min: number, max: number) => (value: Numberish) => Number(value) >= min && Number(value) <= max,

  /* strings */

  maxLength: (length: number) => (s: string) => s.length <= length,
  minLength: (minLength = (length: number) => (s: string) => s.length >= length),
  notEmpty: minLength(1),
  isTrimmed: (s: string) => s.trim() === s,

  /* regex */

  /** Matches a provided regex. */
  isMatching: (isMatching = (r) => (s) => r.test(s)),
  isAlphaNumeric: isMatching(c(ALNUM)),
  isNumeric: isMatching(c(NUMBER)),
  isDecimal: (decimals?: number) => isMatching(r(DECIMAL_TEMPLATE(decimals))),
  isAddress: isMatching(r(ADDRESS)),
  isText: isMatching(c(TEXT)),
  isUUID: isMatching(r(UUID)),
} as const satisfies { [key: string]: Validator | ((...args: any[]) => Validator) }

type Validator = (value: string) => boolean

@Andarist
Copy link
Contributor

Andarist commented Dec 8, 2023

TS can't pick one of the union members here for any given key. This is the same "problem" as this one:

type Valish = (value: string) => boolean;
type CurryValish = (...args: number[]) => Valish;

const fn1: Valish | CurryValish = (value) => true; // implicit anys
const fn2: Valish | CurryValish = (curry) => (value) => true; // implicit anys

And it stems from the fact that you can't really call a union of functions like this. How would you discriminate between members?

type Valish = (value: string) => boolean;
type CurryValish = (...args: number[]) => Valish;

declare const fn: Valish | CurryValish

fn() // const fn: (arg0: never, ...args: number[]) => boolean | Valish

As long as your arguments are different (and you have string and number at the first positions here) that will reduce to never (the parameters are intersected). For example, this one works OK since both functions use number at the first position:

type Valish = (value: number) => boolean;
type CurryValish = (...args: number[]) => Valish;

declare const fn: Valish | CurryValish

fn(10) // ok

This wouldn't help you with those contextual parameter types though, as per #51639 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Not a Defect This behavior is one of several equally-correct options
Projects
None yet
Development

No branches or pull requests

6 participants