Skip to content

explicit return function type produces error when generic function result is returned directly #33394

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
Hotell opened this issue Sep 12, 2019 · 4 comments
Labels
Needs More Info The issue still hasn't been fully clarified

Comments

@Hotell
Copy link

Hotell commented Sep 12, 2019

TypeScript Version: 3.6.3

Search Terms:

  • generics with explicit return type error

Code

// ======================================
// implementation of deepPick ( for 2 nested levels only )
// ======================================

type IfKey<T, K> = [K] extends [keyof T] ? Exclude<Required<T[K]>, undefined> : T
type NextKey<T, K = keyof any> = [K] extends [undefined] ? undefined :
  [keyof T | undefined] extends [K] ? keyof any : (keyof T | undefined)

  function deepPick<
 T0 extends object,
 K1 extends NextKey<T0>,
 T1 extends IfKey<T0,K1>,
 K2 extends NextKey<T1,K1>,
 T2 extends IfKey<T1,K2>,
>(value:T0,path:[K1?,K2?]): T2 | undefined {
  return path.reduce((result: any, key: any) => {
    return result!=null ? result[key] : result;
  }, value);
}

// ======================================
// DATA
// ======================================

type User = {nickname:string; fullName:string; age?:number}
const users: {john: User; doe:User} = {
  john: {
    nickname: 'jo5',
    fullName: 'Johny Five',
    age: 123,
  },
  doe: {
      nickname: 'doe',
      fullName: 'John Doe',
  }
}

// ======================================
// Usage
// ======================================

// 🚨 $ExpectError
/*
 error TS2322: Type '0 | { john: User; doe: User; }' is not assignable to type 'number'.
  Type '{ john: User; doe: User; }' is not assignable to type 'number'.
 return deepPick(users,['john','age']) || 0
*/
function getSomedata(): number {
  return deepPick(users,['john','age']) || 0
}

// 💥NO ERROR
function getSomedataNoErrorAssign(): number {
  const result = deepPick(users,['john','age']) || 0
  return result
}

// 💥NO ERROR
// properly inferred to return `number`
function getSomedataNoError() {
  return deepPick(users,['john','age']) || 0
}

Expected behavior:

Explicitly providing return type should work

Actual behavior:

When explicitly providing return type, TSC will error out on deepPick usage.

In TS 3.5 there are 2 errors:

  1. getSomedata
 error TS2322: Type '0 | { john: User; doe: User; }' is not assignable to type 'number'.
  Type '{ john: User; doe: User; }' is not assignable to type 'number'.

107   return deepPick(users,['john','age']) || 0
  1. getSomedataNoErrorAssign
error TS2322: Type '0 | { john: User; doe: User; }' is not assignable to type 'number'.
  Type '{ john: User; doe: User; }' is not assignable to type 'number'.

112   return result

TS 3.5 vs TS 3.6

TS 3.5

TS 3.6

Playground Link: Demo - NOTE that playground is using old ts 3.5.1

Related Issues: #32937, #33002

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Sep 12, 2019

It seems like this deepPick type won't be needed once optional chaining comes in.

I don't use TS 3.6 yet but hopefully this implementation hasn't broken?

Playground


Overloads seem to work well,

type IfKey<T, K> = [K] extends [keyof T] ? Exclude<Required<T[K]>, undefined> : T

function deepPick<
  T0 extends object
>(value:T0, path:[]): T0;
function deepPick<
  T0 extends object,
  K1 extends keyof T0,
>(value:T0, path:[K1]): IfKey<T0,K1>;
function deepPick<
  T0 extends object,
  K1 extends keyof T0,
  K2 extends keyof IfKey<T0,K1>
>(value:T0, path:[K1,K2]): IfKey<IfKey<T0,K1>,K2>;
function deepPick(value:any, path:[string?, string?]): any {
  return path.reduce((value: any, key: any) => {
    return value && value[key];
  }, value);
}

type User = {nickname:string;fullName:string;age?:number}

const users: {john: User;doe:User} = {
  john: {
    nickname: 'jo5',
    fullName: 'Johny Five',
    age: 123,
  },
  doe: {
      nickname: 'doe',
      fullName: 'John Doe',
      age: Infinity
  }
}

function getSomedata(): number {
  return deepPick(users,['john','age']) || 0
}

function getSomedataNoErrorAssign(): number {
  const result = deepPick(users,['john','age']) || 0
  return result
}

function getSomedataNoError() {
  return deepPick(users,['john','age']) || 0
}

Playground

@RyanCavanaugh
Copy link
Member

Is there a simpler reproduction of the difference here?

@RyanCavanaugh RyanCavanaugh added the Needs More Info The issue still hasn't been fully clarified label Sep 13, 2019
@Hotell
Copy link
Author

Hotell commented Sep 18, 2019

@AnyhowStep

It seems like this deepPick type won't be needed once optional chaining comes in.

Yeah for most of the use cases it wont, although for non esNext browsers, transpilation of optional chaining is quiet heavy in terms of js bundle size.

Overloads seem to work well,

Actually we use similar overload solution as you proposed, which errors out in 3.6
image

image

@Hotell
Copy link
Author

Hotell commented Sep 18, 2019

So no difference @RyanCavanaugh both implementations are producing same error in 3.6 👀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs More Info The issue still hasn't been fully clarified
Projects
None yet
Development

No branches or pull requests

3 participants