Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript: Discriminated Unions with optional values

Tags:

typescript

Given the following types:

interface FullName {
  fullName?: string
}

interface Name {
  firstName: string
  lastName: string
}

type Person = FullName | Name;

const p1: Person = {};
const p2: Person = { fullName: 'test' };
const p3: Person = { firstName: 'test' }; // Does not throw
const p4: Person = { badProp: true }; // Does throw, as badProp is not on FullName | Name;

I would expect p3 to result in a compiler error, as firstName is present without lastName, but it doesn't -- is this a bug or expected?

Additionally, making FullName.fullName required results in p3 (and p1) causing errors.

like image 399
Ben Southgate Avatar asked Jan 05 '17 21:01

Ben Southgate


People also ask

What is discriminated unions in TypeScript?

Discriminated union is a data structure used to hold a value that could take on different, fixed types. These are basically union types with a tag. To convert a union type into a discriminated union type, we use a common property across our types.

What is tagged union in TypeScript?

Creating Tagged Unions in TypeScript Introduction to Tagged Unions A tagged union is a data structure that holds several different data types, each of them distinguishable from one another using a discriminating property (usually called a “tag”).

Does not exist on type Union TypeScript?

The "property does not exist on type union" error occurs when we try to access a property that is not present on every object in the union type. To solve the error, use a type guard to ensure the property exists on the object before accessing it.

When would you use an intersection type instead of a union type?

Intersection types are closely related to union types, but they are used very differently. An intersection type combines multiple types into one. This allows you to add together existing types to get a single type that has all the features you need.


2 Answers

First of, your interface FullName does only contain one optional property, that is basically making it match anything. Then when you do a union type with it, the resulting type is going to be compatible with everything.

However, there is another concern considering declaring and assigning literal objects, and that is that you only can declare known properties: Why am I getting an error "Object literal may only specify known properties"?

So you could do this without any problem:

var test = { otherStuff: 23 };
const p4: Person = test;

But not this

const p4: Person = { otherStuff: 23 };

And in your case firstName is a known property of FullName | Name, so it's all ok.

And as @artem answered, discriminated unions have a special meaning in typescript, apart from regular unions, requiring special structural assumptions.

like image 164
Alex Avatar answered Oct 21 '22 12:10

Alex


The type in your question is not, in usual sense, discriminated union - your union members don't have common, non-optional literal property called discriminant.

So, as @Alex noted in his answer, your union is somewhat similar to

type Person = {
  fullName?: string
  firstName?: string
  lastName?: string
}

so it can be initialized with { firstName: 'test' }

With true discriminated unions, you get back the logic for checking non-optional properties, as well as checking that object literal may only specify known properties:

interface FullName {
  kind: 'fullname';  
  fullName?: string
}

interface Name {
  kind: 'name';  
  firstName: string
  lastName: string
}

type Person = FullName | Name;

const p1: Person = {kind: 'fullname'};  // ok
const p2: Person = {kind: 'fullname', fullName: 'test' };  // ok

checking non-optional property:

const p3: Person = {kind: 'name', firstName: 'test' }; 

error:

Type '{ kind: "name"; firstName: string; }' is not assignable to type 'Person'.
  Type '{ kind: "name"; firstName: string; }' is not assignable to type 'Name'.
    Property 'lastName' is missing in type '{ kind: "name"; firstName: string; }'.

checking extra property:

const p5: Person = { kind: 'fullname', bar: 42 }

error:

Type '{ kind: "fullname"; bar: number; }' is not assignable to type 'Person'.
  Object literal may only specify known properties, and 'bar' does not exist in type 'Person'.

However, as @JeffMercado found out, type checking is still a bit off:

const p6: Person = { kind: 'fullname', firstName: 42 };  // no error. why?

I'd consider posting an issue for typescript github project.

like image 41
artem Avatar answered Oct 21 '22 12:10

artem