0

I need to create type which it's behaviour would be choosing between one of given interfaces.

Please note: in the example below someTask is data returned from the server! There is no actual initiation of it in the code! For example:

interface TaskA { foo: string }

interface TaskB { fee: string }

type Task = TaskB | TaskB 

const task:Task = someTask

console.log(task.fee)

The console.log function will yell at me:

Property 'fee' does not exist on type 'Task'.

What am I doing wrong here?

7
  • 1
    const task:Task might be TaskA and TaskA has no fee. Commented Nov 6, 2019 at 15:11
  • And it actually isn't there probably. where is someTask initiated? Commented Nov 6, 2019 at 15:11
  • @HaimHouri The thing is I am building types for data that is returning from the server, therefor there is no fee at the moment I guess I am handling it wrong than. Any idea how can I make it work in this scenario? Commented Nov 6, 2019 at 15:16
  • @obiwankenoobi try doing `console.log(task.fee || task.foo). does this helps? Commented Nov 6, 2019 at 15:19
  • @HaimHouri even if it does, this example is obvious toy example. irl the interfaces will be huge and you can't really compare them like that every time. It's so weird there is no good way to deal with it Commented Nov 6, 2019 at 15:21

3 Answers 3

1

The terms "union" and "intersection" as used in Typescript are mathematically correct, but still a bit confusing when applied to objects/interfaces. A union of two interfaces A and B is a type for all objects which are A or B. To work with such type reliably, the compiler has to be sure that only keys that are present in both A and B are used. In other words, a union of objects contains only an intersection of their keys. Conversely, an intersection of A and B, A & B, contains all keys from both A and B, that is, an intersection of interfaces is a union of their keys:

type A = { a: 1, b: 2, x: 3 }
type B = { a: 1, b: 2, y: 3 }

type U = keyof (A | B)  // a|b
type I = keyof (A & B)  // a|b|x|y

Since your objects don't have any common keys, their union is essentially an empty object ({never: any}). It cannot have any properties.

What you can do, is to introduce a tagged union, in which all member types have a common property ("tag") which enables the compiler to differentiate between them. Example:

type SomeTask<T, P extends {}> = { type: T } & P

type TaskOne = SomeTask<'first', {
    one: number
}>

type TaskTwo = SomeTask<'second', {
    two: number
}>

type Task = TaskOne | TaskTwo;

function validateTask(t: Task) {
    if (t.type === 'first')
        console.log(t.one);
    if (t.type === 'second')
        console.log(t.two);
}

Here, the compiler knows, that if the type is first, then the object is expected to have the one property.

Sign up to request clarification or add additional context in comments.

Comments

0

It should work.

Make sure that you have defined property 'fee' in your object.

enter image description here

Comments

0

If you can't be sure that the object you get from your server is a Task, you should add a User Defined Type Guard for either TaskA or TaskB. That way you can be sure that someTask has fee as a property, in this case.

interface TaskA { foo: string }

interface TaskB { fee: string }

type Task = TaskA | TaskB 

const task: Task = {fee: 'bar'};

console.log(isTaskA(task) ? task.foo : task.fee)

function isTaskA(value: TaskA | TaskB): value is TaskA {
    return value.hasOwnProperty('foo');
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.