简体   繁体   中英

TypeScript: Discriminated Unions with optional values

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.

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 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.

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.

2021 Update : The example in the question now works as intended.

Since at least TypeScript version 3.3.3 (the oldest version one can currently test on the TypeScript playground) you don't need a discriminant (ie a common, non-optional literal property).

Given

interface FullName {
  fullName?: string
}

interface Name {
  firstName: string
  lastName: string
}

type Person = FullName | Name;

as in the question, the following example (marked as "Does not throw" by the person asking this question)

const p3: Person = { firstName: 'test' }; // Does not throw

now leads to this TypeScript error:

Property 'lastName' is missing in type '{ firstName: string; }' but required in type 'Name'.

And @artem was wondering why

const p6: Person = { kind: 'fullname', firstName: 42 };

would not throw an error in his example with the discriminant kind .

Well, since at last TypeScript version 3.3.3 it does throw an error, precisely the expected one:

Object literal may only specify known properties, and 'firstName' does not exist in type 'FullName'.

See this TypeScript playground which includes both examples (with and without discriminated union).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM