How is a function able to know the exact return value depending on whether the parameter is undefined or not?
up vote
1
down vote
favorite
interface Activity {
eat: () => void
}
interface Person {
activity?: Activity
}
const activity = <T extends Person>(person: T) => ({
eat: person.activity && person.activity.eat
})
const tom = {
activity: {
eat: () => {}
}
}
const tomAct = activity(tom)
tomAct.eat() // should know `eat` does exist
const bobAct = activity({})
bobAct.eat // should know `eat` is undefined
You can see tomAct.eat will return eat: (() => void) | undefined
but tomAct
in this case knowns that eat: (() => void
and bobAct is known undefined
.
Does Typescript support this case? How can I solve that?
===
"typescript": "^3.1.2",
typescript
add a comment |
up vote
1
down vote
favorite
interface Activity {
eat: () => void
}
interface Person {
activity?: Activity
}
const activity = <T extends Person>(person: T) => ({
eat: person.activity && person.activity.eat
})
const tom = {
activity: {
eat: () => {}
}
}
const tomAct = activity(tom)
tomAct.eat() // should know `eat` does exist
const bobAct = activity({})
bobAct.eat // should know `eat` is undefined
You can see tomAct.eat will return eat: (() => void) | undefined
but tomAct
in this case knowns that eat: (() => void
and bobAct is known undefined
.
Does Typescript support this case? How can I solve that?
===
"typescript": "^3.1.2",
typescript
add a comment |
up vote
1
down vote
favorite
up vote
1
down vote
favorite
interface Activity {
eat: () => void
}
interface Person {
activity?: Activity
}
const activity = <T extends Person>(person: T) => ({
eat: person.activity && person.activity.eat
})
const tom = {
activity: {
eat: () => {}
}
}
const tomAct = activity(tom)
tomAct.eat() // should know `eat` does exist
const bobAct = activity({})
bobAct.eat // should know `eat` is undefined
You can see tomAct.eat will return eat: (() => void) | undefined
but tomAct
in this case knowns that eat: (() => void
and bobAct is known undefined
.
Does Typescript support this case? How can I solve that?
===
"typescript": "^3.1.2",
typescript
interface Activity {
eat: () => void
}
interface Person {
activity?: Activity
}
const activity = <T extends Person>(person: T) => ({
eat: person.activity && person.activity.eat
})
const tom = {
activity: {
eat: () => {}
}
}
const tomAct = activity(tom)
tomAct.eat() // should know `eat` does exist
const bobAct = activity({})
bobAct.eat // should know `eat` is undefined
You can see tomAct.eat will return eat: (() => void) | undefined
but tomAct
in this case knowns that eat: (() => void
and bobAct is known undefined
.
Does Typescript support this case? How can I solve that?
===
"typescript": "^3.1.2",
typescript
typescript
edited Nov 9 at 9:05
marc_s
566k12610921245
566k12610921245
asked Nov 9 at 8:06
Le Tom
649
649
add a comment |
add a comment |
2 Answers
2
active
oldest
votes
up vote
1
down vote
accepted
Your problem is that control flow analysis doesn't really work very well on generics. The compiler is essentially widening T
to Person
for the purposes of control flow analysis (figuring out what type person.activity && person.activity.eat
will be), so the inferred return type of activity()
is the same as that of the concrete (non-generic) version of the function:
const activityConcrete = (person: Person) => ({
eat: person.activity && person.activity.eat
}); // {eat: ()=>void | undefined}
In order to get the behavior you want you either need to walk the compiler through the analysis (which is sometimes impossible) or just assert the return type you expect. Traditionally what you'd do here is to use overloads to represent the relationship between input and output types:
function activity(person: { activity: Activity }): Activity;
function activity(person: { activity?: undefined }): { eat: undefined };
function activity(person: Person): { eat: Activity['eat'] | undefined };
function activity(person: Person): { eat: Activity['eat'] | undefined } {
return {
eat: person.activity && person.activity.eat
}
}
As of TypeScript 2.8 you can use conditional types to represent the same thing:
type PersonEat<T extends Person> = T['activity'] extends infer A ?
A extends Activity ? A['eat'] : undefined : never;
const activity = <T extends Person>(person: T) => ({
eat: person.activity && person.activity.eat
} as { eat: PersonEat<T> })
Either way should result in similar behavior:
const tom = {
activity: {
eat: () => {}
}
}
const bob = {};
const tomAct = activity(tom)
tomAct.eat() // okay 🙂
const bobAct = activity(bob)
bobAct.eat // undefined 🙂
So that works.
Please note that there's a bit of a wrinkle with how it treats a Person
without an activity
. The type of bob
above is {}
, which is treated as a top type for objects, meaning that it absorbs any other object type you union with it. That is, in:
const tomOrBob = Math.random() < 0.5 ? tom : bob; // type is {}
it is inferred that tomOrBob
is of type {} | {activity: Activity}
, which is collapsed to just {}
. So the compiler forgets that tomOrBob
might have an activity
. And that leads to the following incorrect behavior:
const tomOrBobActivity = activity(tomOrBob);
tomOrBobActivity.eat; // undefined 🙁 but it should be (()=>void) | undefined
If you're okay with that overzealous undefinedness, fine. Otherwise, you need to explicitly tell the compiler to remember that activity
is missing from bob
:
const bob: { activity?: undefined } = {}; // bob definitely is missing activity
const bobAct = activity(bob);
bobAct.eat // still undefined as desired 🙂
const tomOrBob = Math.random() < 0.5 ? tom : bob;
const tomOrBobAct = activity(tomOrBob);
tomOrBobAct.eat; // (() => void) | undefined 🙂
And that behaves as desired.
Okay, hope that helps. Good luck!
Thank you for the detailed and informative answer @jcal.
– Le Tom
Nov 12 at 2:52
One more question about definitionPersonEat
why you usenever
in the end, I though it will be undefined. Could you tell me what i'm wrong?
– Le Tom
Nov 12 at 3:06
1
The constructionX extends infer Y ? Z : never
uses conditional type inference to assignX
to a new type variableY
. The conditional typeX extends infer Y
will always be satisfied, so thenever
is ignored. In the case ofPersonEat
, I wanted to makeT['activity']
distribute across a union, but you need a "naked" type parameter likeA
, so I reassigned it.
– jcalz
Nov 12 at 3:45
1
If it's too confusing, I'd recommend staying with the overloads.
– jcalz
Nov 12 at 3:45
add a comment |
up vote
1
down vote
Typescript is a transpiler that works on compile time, therefore it can know only the things that are known at that time.
You requirement is runtime requirement, the value of some property will be known only at runtime, therefore it is not possible to do with TS.
add a comment |
2 Answers
2
active
oldest
votes
2 Answers
2
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
1
down vote
accepted
Your problem is that control flow analysis doesn't really work very well on generics. The compiler is essentially widening T
to Person
for the purposes of control flow analysis (figuring out what type person.activity && person.activity.eat
will be), so the inferred return type of activity()
is the same as that of the concrete (non-generic) version of the function:
const activityConcrete = (person: Person) => ({
eat: person.activity && person.activity.eat
}); // {eat: ()=>void | undefined}
In order to get the behavior you want you either need to walk the compiler through the analysis (which is sometimes impossible) or just assert the return type you expect. Traditionally what you'd do here is to use overloads to represent the relationship between input and output types:
function activity(person: { activity: Activity }): Activity;
function activity(person: { activity?: undefined }): { eat: undefined };
function activity(person: Person): { eat: Activity['eat'] | undefined };
function activity(person: Person): { eat: Activity['eat'] | undefined } {
return {
eat: person.activity && person.activity.eat
}
}
As of TypeScript 2.8 you can use conditional types to represent the same thing:
type PersonEat<T extends Person> = T['activity'] extends infer A ?
A extends Activity ? A['eat'] : undefined : never;
const activity = <T extends Person>(person: T) => ({
eat: person.activity && person.activity.eat
} as { eat: PersonEat<T> })
Either way should result in similar behavior:
const tom = {
activity: {
eat: () => {}
}
}
const bob = {};
const tomAct = activity(tom)
tomAct.eat() // okay 🙂
const bobAct = activity(bob)
bobAct.eat // undefined 🙂
So that works.
Please note that there's a bit of a wrinkle with how it treats a Person
without an activity
. The type of bob
above is {}
, which is treated as a top type for objects, meaning that it absorbs any other object type you union with it. That is, in:
const tomOrBob = Math.random() < 0.5 ? tom : bob; // type is {}
it is inferred that tomOrBob
is of type {} | {activity: Activity}
, which is collapsed to just {}
. So the compiler forgets that tomOrBob
might have an activity
. And that leads to the following incorrect behavior:
const tomOrBobActivity = activity(tomOrBob);
tomOrBobActivity.eat; // undefined 🙁 but it should be (()=>void) | undefined
If you're okay with that overzealous undefinedness, fine. Otherwise, you need to explicitly tell the compiler to remember that activity
is missing from bob
:
const bob: { activity?: undefined } = {}; // bob definitely is missing activity
const bobAct = activity(bob);
bobAct.eat // still undefined as desired 🙂
const tomOrBob = Math.random() < 0.5 ? tom : bob;
const tomOrBobAct = activity(tomOrBob);
tomOrBobAct.eat; // (() => void) | undefined 🙂
And that behaves as desired.
Okay, hope that helps. Good luck!
Thank you for the detailed and informative answer @jcal.
– Le Tom
Nov 12 at 2:52
One more question about definitionPersonEat
why you usenever
in the end, I though it will be undefined. Could you tell me what i'm wrong?
– Le Tom
Nov 12 at 3:06
1
The constructionX extends infer Y ? Z : never
uses conditional type inference to assignX
to a new type variableY
. The conditional typeX extends infer Y
will always be satisfied, so thenever
is ignored. In the case ofPersonEat
, I wanted to makeT['activity']
distribute across a union, but you need a "naked" type parameter likeA
, so I reassigned it.
– jcalz
Nov 12 at 3:45
1
If it's too confusing, I'd recommend staying with the overloads.
– jcalz
Nov 12 at 3:45
add a comment |
up vote
1
down vote
accepted
Your problem is that control flow analysis doesn't really work very well on generics. The compiler is essentially widening T
to Person
for the purposes of control flow analysis (figuring out what type person.activity && person.activity.eat
will be), so the inferred return type of activity()
is the same as that of the concrete (non-generic) version of the function:
const activityConcrete = (person: Person) => ({
eat: person.activity && person.activity.eat
}); // {eat: ()=>void | undefined}
In order to get the behavior you want you either need to walk the compiler through the analysis (which is sometimes impossible) or just assert the return type you expect. Traditionally what you'd do here is to use overloads to represent the relationship between input and output types:
function activity(person: { activity: Activity }): Activity;
function activity(person: { activity?: undefined }): { eat: undefined };
function activity(person: Person): { eat: Activity['eat'] | undefined };
function activity(person: Person): { eat: Activity['eat'] | undefined } {
return {
eat: person.activity && person.activity.eat
}
}
As of TypeScript 2.8 you can use conditional types to represent the same thing:
type PersonEat<T extends Person> = T['activity'] extends infer A ?
A extends Activity ? A['eat'] : undefined : never;
const activity = <T extends Person>(person: T) => ({
eat: person.activity && person.activity.eat
} as { eat: PersonEat<T> })
Either way should result in similar behavior:
const tom = {
activity: {
eat: () => {}
}
}
const bob = {};
const tomAct = activity(tom)
tomAct.eat() // okay 🙂
const bobAct = activity(bob)
bobAct.eat // undefined 🙂
So that works.
Please note that there's a bit of a wrinkle with how it treats a Person
without an activity
. The type of bob
above is {}
, which is treated as a top type for objects, meaning that it absorbs any other object type you union with it. That is, in:
const tomOrBob = Math.random() < 0.5 ? tom : bob; // type is {}
it is inferred that tomOrBob
is of type {} | {activity: Activity}
, which is collapsed to just {}
. So the compiler forgets that tomOrBob
might have an activity
. And that leads to the following incorrect behavior:
const tomOrBobActivity = activity(tomOrBob);
tomOrBobActivity.eat; // undefined 🙁 but it should be (()=>void) | undefined
If you're okay with that overzealous undefinedness, fine. Otherwise, you need to explicitly tell the compiler to remember that activity
is missing from bob
:
const bob: { activity?: undefined } = {}; // bob definitely is missing activity
const bobAct = activity(bob);
bobAct.eat // still undefined as desired 🙂
const tomOrBob = Math.random() < 0.5 ? tom : bob;
const tomOrBobAct = activity(tomOrBob);
tomOrBobAct.eat; // (() => void) | undefined 🙂
And that behaves as desired.
Okay, hope that helps. Good luck!
Thank you for the detailed and informative answer @jcal.
– Le Tom
Nov 12 at 2:52
One more question about definitionPersonEat
why you usenever
in the end, I though it will be undefined. Could you tell me what i'm wrong?
– Le Tom
Nov 12 at 3:06
1
The constructionX extends infer Y ? Z : never
uses conditional type inference to assignX
to a new type variableY
. The conditional typeX extends infer Y
will always be satisfied, so thenever
is ignored. In the case ofPersonEat
, I wanted to makeT['activity']
distribute across a union, but you need a "naked" type parameter likeA
, so I reassigned it.
– jcalz
Nov 12 at 3:45
1
If it's too confusing, I'd recommend staying with the overloads.
– jcalz
Nov 12 at 3:45
add a comment |
up vote
1
down vote
accepted
up vote
1
down vote
accepted
Your problem is that control flow analysis doesn't really work very well on generics. The compiler is essentially widening T
to Person
for the purposes of control flow analysis (figuring out what type person.activity && person.activity.eat
will be), so the inferred return type of activity()
is the same as that of the concrete (non-generic) version of the function:
const activityConcrete = (person: Person) => ({
eat: person.activity && person.activity.eat
}); // {eat: ()=>void | undefined}
In order to get the behavior you want you either need to walk the compiler through the analysis (which is sometimes impossible) or just assert the return type you expect. Traditionally what you'd do here is to use overloads to represent the relationship between input and output types:
function activity(person: { activity: Activity }): Activity;
function activity(person: { activity?: undefined }): { eat: undefined };
function activity(person: Person): { eat: Activity['eat'] | undefined };
function activity(person: Person): { eat: Activity['eat'] | undefined } {
return {
eat: person.activity && person.activity.eat
}
}
As of TypeScript 2.8 you can use conditional types to represent the same thing:
type PersonEat<T extends Person> = T['activity'] extends infer A ?
A extends Activity ? A['eat'] : undefined : never;
const activity = <T extends Person>(person: T) => ({
eat: person.activity && person.activity.eat
} as { eat: PersonEat<T> })
Either way should result in similar behavior:
const tom = {
activity: {
eat: () => {}
}
}
const bob = {};
const tomAct = activity(tom)
tomAct.eat() // okay 🙂
const bobAct = activity(bob)
bobAct.eat // undefined 🙂
So that works.
Please note that there's a bit of a wrinkle with how it treats a Person
without an activity
. The type of bob
above is {}
, which is treated as a top type for objects, meaning that it absorbs any other object type you union with it. That is, in:
const tomOrBob = Math.random() < 0.5 ? tom : bob; // type is {}
it is inferred that tomOrBob
is of type {} | {activity: Activity}
, which is collapsed to just {}
. So the compiler forgets that tomOrBob
might have an activity
. And that leads to the following incorrect behavior:
const tomOrBobActivity = activity(tomOrBob);
tomOrBobActivity.eat; // undefined 🙁 but it should be (()=>void) | undefined
If you're okay with that overzealous undefinedness, fine. Otherwise, you need to explicitly tell the compiler to remember that activity
is missing from bob
:
const bob: { activity?: undefined } = {}; // bob definitely is missing activity
const bobAct = activity(bob);
bobAct.eat // still undefined as desired 🙂
const tomOrBob = Math.random() < 0.5 ? tom : bob;
const tomOrBobAct = activity(tomOrBob);
tomOrBobAct.eat; // (() => void) | undefined 🙂
And that behaves as desired.
Okay, hope that helps. Good luck!
Your problem is that control flow analysis doesn't really work very well on generics. The compiler is essentially widening T
to Person
for the purposes of control flow analysis (figuring out what type person.activity && person.activity.eat
will be), so the inferred return type of activity()
is the same as that of the concrete (non-generic) version of the function:
const activityConcrete = (person: Person) => ({
eat: person.activity && person.activity.eat
}); // {eat: ()=>void | undefined}
In order to get the behavior you want you either need to walk the compiler through the analysis (which is sometimes impossible) or just assert the return type you expect. Traditionally what you'd do here is to use overloads to represent the relationship between input and output types:
function activity(person: { activity: Activity }): Activity;
function activity(person: { activity?: undefined }): { eat: undefined };
function activity(person: Person): { eat: Activity['eat'] | undefined };
function activity(person: Person): { eat: Activity['eat'] | undefined } {
return {
eat: person.activity && person.activity.eat
}
}
As of TypeScript 2.8 you can use conditional types to represent the same thing:
type PersonEat<T extends Person> = T['activity'] extends infer A ?
A extends Activity ? A['eat'] : undefined : never;
const activity = <T extends Person>(person: T) => ({
eat: person.activity && person.activity.eat
} as { eat: PersonEat<T> })
Either way should result in similar behavior:
const tom = {
activity: {
eat: () => {}
}
}
const bob = {};
const tomAct = activity(tom)
tomAct.eat() // okay 🙂
const bobAct = activity(bob)
bobAct.eat // undefined 🙂
So that works.
Please note that there's a bit of a wrinkle with how it treats a Person
without an activity
. The type of bob
above is {}
, which is treated as a top type for objects, meaning that it absorbs any other object type you union with it. That is, in:
const tomOrBob = Math.random() < 0.5 ? tom : bob; // type is {}
it is inferred that tomOrBob
is of type {} | {activity: Activity}
, which is collapsed to just {}
. So the compiler forgets that tomOrBob
might have an activity
. And that leads to the following incorrect behavior:
const tomOrBobActivity = activity(tomOrBob);
tomOrBobActivity.eat; // undefined 🙁 but it should be (()=>void) | undefined
If you're okay with that overzealous undefinedness, fine. Otherwise, you need to explicitly tell the compiler to remember that activity
is missing from bob
:
const bob: { activity?: undefined } = {}; // bob definitely is missing activity
const bobAct = activity(bob);
bobAct.eat // still undefined as desired 🙂
const tomOrBob = Math.random() < 0.5 ? tom : bob;
const tomOrBobAct = activity(tomOrBob);
tomOrBobAct.eat; // (() => void) | undefined 🙂
And that behaves as desired.
Okay, hope that helps. Good luck!
answered Nov 9 at 15:09
jcalz
20.4k21535
20.4k21535
Thank you for the detailed and informative answer @jcal.
– Le Tom
Nov 12 at 2:52
One more question about definitionPersonEat
why you usenever
in the end, I though it will be undefined. Could you tell me what i'm wrong?
– Le Tom
Nov 12 at 3:06
1
The constructionX extends infer Y ? Z : never
uses conditional type inference to assignX
to a new type variableY
. The conditional typeX extends infer Y
will always be satisfied, so thenever
is ignored. In the case ofPersonEat
, I wanted to makeT['activity']
distribute across a union, but you need a "naked" type parameter likeA
, so I reassigned it.
– jcalz
Nov 12 at 3:45
1
If it's too confusing, I'd recommend staying with the overloads.
– jcalz
Nov 12 at 3:45
add a comment |
Thank you for the detailed and informative answer @jcal.
– Le Tom
Nov 12 at 2:52
One more question about definitionPersonEat
why you usenever
in the end, I though it will be undefined. Could you tell me what i'm wrong?
– Le Tom
Nov 12 at 3:06
1
The constructionX extends infer Y ? Z : never
uses conditional type inference to assignX
to a new type variableY
. The conditional typeX extends infer Y
will always be satisfied, so thenever
is ignored. In the case ofPersonEat
, I wanted to makeT['activity']
distribute across a union, but you need a "naked" type parameter likeA
, so I reassigned it.
– jcalz
Nov 12 at 3:45
1
If it's too confusing, I'd recommend staying with the overloads.
– jcalz
Nov 12 at 3:45
Thank you for the detailed and informative answer @jcal.
– Le Tom
Nov 12 at 2:52
Thank you for the detailed and informative answer @jcal.
– Le Tom
Nov 12 at 2:52
One more question about definition
PersonEat
why you use never
in the end, I though it will be undefined. Could you tell me what i'm wrong?– Le Tom
Nov 12 at 3:06
One more question about definition
PersonEat
why you use never
in the end, I though it will be undefined. Could you tell me what i'm wrong?– Le Tom
Nov 12 at 3:06
1
1
The construction
X extends infer Y ? Z : never
uses conditional type inference to assign X
to a new type variable Y
. The conditional type X extends infer Y
will always be satisfied, so the never
is ignored. In the case of PersonEat
, I wanted to make T['activity']
distribute across a union, but you need a "naked" type parameter like A
, so I reassigned it.– jcalz
Nov 12 at 3:45
The construction
X extends infer Y ? Z : never
uses conditional type inference to assign X
to a new type variable Y
. The conditional type X extends infer Y
will always be satisfied, so the never
is ignored. In the case of PersonEat
, I wanted to make T['activity']
distribute across a union, but you need a "naked" type parameter like A
, so I reassigned it.– jcalz
Nov 12 at 3:45
1
1
If it's too confusing, I'd recommend staying with the overloads.
– jcalz
Nov 12 at 3:45
If it's too confusing, I'd recommend staying with the overloads.
– jcalz
Nov 12 at 3:45
add a comment |
up vote
1
down vote
Typescript is a transpiler that works on compile time, therefore it can know only the things that are known at that time.
You requirement is runtime requirement, the value of some property will be known only at runtime, therefore it is not possible to do with TS.
add a comment |
up vote
1
down vote
Typescript is a transpiler that works on compile time, therefore it can know only the things that are known at that time.
You requirement is runtime requirement, the value of some property will be known only at runtime, therefore it is not possible to do with TS.
add a comment |
up vote
1
down vote
up vote
1
down vote
Typescript is a transpiler that works on compile time, therefore it can know only the things that are known at that time.
You requirement is runtime requirement, the value of some property will be known only at runtime, therefore it is not possible to do with TS.
Typescript is a transpiler that works on compile time, therefore it can know only the things that are known at that time.
You requirement is runtime requirement, the value of some property will be known only at runtime, therefore it is not possible to do with TS.
answered Nov 9 at 9:36
felixmosh
3,6722517
3,6722517
add a comment |
add a comment |
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53221898%2fhow-is-a-function-able-to-know-the-exact-return-value-depending-on-whether-the-p%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown