4

Is there a way to make TypeScript compiler produce an error when a function is called with an argument that can be both of the union type cases? Example:

interface Name {
  name: string
}

interface Email {
  email: string
}

type NameOrEmail = Name | Email

function print(p: NameOrEmail) {
  console.log(p)
}

print({name: 'alice'}) // Works
print({email: 'alice'}) // Works
print({email: 'alice', name: 'alice'}) // Works, but I'd like it to fail
print({email: 'alice', age: 33}) // Doesn't work
synapse
  • 5,588
  • 6
  • 35
  • 65

4 Answers4

4

You can use method overloading:

interface Name {
  name: string
}

interface Email {
  email: string
}

function print(p: Name): void;
function print(p: Email): void;
function print(p: Name | Email) {
  console.log(p);
}

print({name: 'alice'}) // Works
print({email: 'alice'}) // Works
print({email: 'alice', name: 'alice'}) // Doesn't work
print({email: 'alice', age: 33}) // Doesn't work

This will basically make the signature of the method implementation "invisible" to the rest of your code.

Demo

Edit:

As pointed out by @str in strict mode the overload signatures need to specify a return type. It's still possible for the implementation to have it's return type be inferred as long as it's compatible to the return types specified in the visible signatures.

Tao
  • 2,105
  • 14
  • 15
  • This is amazing! Just a small improvement: According to the [overloads documentation](https://www.typescriptlang.org/docs/handbook/functions.html#overloads), you don't need to write `print(p: Name | Email)` but `print(p)` is enough. – str Feb 13 '18 at 13:06
  • TypeScript won't infer the type from the visible signatures. `print(p)` is the same as `print(p: any)`. In this case it doesn't really matter but I think it's better to provide the types and use typeguards in the function body. Actually, if you copy the example from the documentation into the playground and activate `noImplicityAny` the compiler will complain. – Tao Feb 13 '18 at 13:23
  • 1
    You are correct about type inference, my bad. Regarding `noImplicityAny`, yes it will complain but so will it for every overload as there is no return value specified. – str Feb 13 '18 at 13:30
  • You're right, missed that, thanks! Will update the answer. – Tao Feb 13 '18 at 13:33
3

TypeScript does not support exclusive union types (yet).

There is an open issue with a proposed syntax and discussion here: Proposal: Allow exclusive unions using logical or (^) operator between types

str
  • 42,689
  • 17
  • 109
  • 127
  • Thanks for the link. It seems that there is a workaround - declare properties from other cases of the union as optional with type `never`. Though when a program is modified this way, `tsc` throws `error TS2384: Overload signatures must all be ambient or non-ambient.` at function declaration. – synapse Feb 13 '18 at 12:54
2

The way the check for excess properties on object literals is performed on union types, if a property is present on any member of the union it will not trigger an error. If as in your case, the interfaces don't have incompatible properties, the object literal with excess properties will then be assignable to either member of the union so it will be considered a valid assignment.

The only way top avoid this behavior is to make the interfaces incompatible, for example by adding a field that is of a string literal type with a different value in each interface:

interface Name {
    type: 'n'
    name: string
}

interface Email {
    type: 'e'
    email: string
}

type NameOrEmail = Name | Email

function print(p: NameOrEmail) {
    console.log(p)
}

print({ name: 'alice', type: 'n' }) // Works
print({ email: 'alice', type: 'e' }) // Works
print({ email: 'alice', name: 'alice', type: 'e' }) //Will fail 
print({ email: 'alice', age: 33, type: 'e' }) // Doesn't work
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
0

Here is another example:

export type UnionType = Type1 | Type2;

export interface Type1 {
    command: number;
}

export interface Type2 {
    children: string;
}

export const unionType: UnionType = {
    command: 34234,
    children: [{ foo: 'qwerty' }, { asdf: 'zxcv' }], // <-- this is fine for Typescript!
};

Here is the link

Eric Aya
  • 69,473
  • 35
  • 181
  • 253
user2010955
  • 3,871
  • 7
  • 34
  • 53