## Union types
🔹 *parent* [[⧋ Typescript|Typescript]]
▫️ *prev* [[⏶ Basic types|Basic types]], *next* [[⏶ Decorators and Dynamic Behavior (Typescript)|Decorators]], [[⏶ Module basics|Module basics]]
### Union types
Union types allow you to define multiple options for a interface definition, so that only one of the options will be allowed.
- define multiple aliases
- define enum on one line
- combine two interfaces with `&`
- infer type with function `if/else`
```ts
// create alias
type ContactBirthDate = Date | number | string
// combined alias instead of enum type
type ContactStatus = "active" | "inactive" | "new"
/* // instead of
enum ContactStatus {
Active = "active",
Inactive = "inactive",
New = "new"
}
*/
interface Contact {
id: number;
name: string;
birthdate?: ContactBirthDate; // alias defined above
status?: ContactStatus;
}
interface Address {
line1: string;
line2: string;
province: string;
region: string;
postalCode: string;
}
type AddressableContact = Contact & Address // combines above easily
function getBirthDate(contact: Contact) {
if (typeof contact.birthDate === "number") {
return new Date(contact.birthDate)
}
else {
return Date.parse(contact.birthDate) //
}
else { // knows that it's neither a date or a number, must be string
return contact.birthDate
}
}
```
### The `keyof` operator
The keyof operator lets you define a variable as the union of keys.
```ts
type Point = { x: number; y: number };
type P = keyof Point; // same as, type P = X | Y
```
This also will be effected by coercion
```ts
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;
// type A = number
type Mapish = { [k: string]: boolean };
type M = keyof Mapish;
// type M = string | number
// since object keys coerce to a string, obj[0] == obj["0"]
```
### The `typeof` operator
The `typeof` operator can check if a passed value is a certain type and then return specific values.
```ts
type ContactName = string;
type ContactBirthDate = string | number | Date;
type ContactStatus = 'active' | 'inactive' | 'new';
interface Contact {
id: number;
name: string;
birthDate?: ContactBirthDate;
status?: ContactStatus;
}
function toContact(nameOrContact: string | Contact): Contact {
if (typeof nameOrContact === "object") {
return {
id: nameOrContact.id,
name: nameOrContact.name,
status: nameOrContact.status
}
}
else {
return {
id: 0,
name: nameOrContact,
status: 'active'
}
}
}
```
Also you can use `typeof` to make sure that a parameter matches a given structure.
```ts
const myType = { min: 1, max: 200 };
// won't work unless save({min:number, max:number})
function save(source: typeof myType) {
console.log(source.min, source.max);
}
save({ min: 20, max: 40 }); // 20 40
```
### Indexed access types
```ts
type ContactStatus = 'active' | 'inactive' | 'new';
interface Address {
street: string;
province: string;
postalCode: string;
}
interface Contact {
id: number;
name: string;
status: ContactStatus;
address: Address
}
type Awesome = Contact["address"]["postalCode"] // complex selector
interface ContactEvent {
contactId: Contact["id"];
}
interface ContactDeletedEvent extends ContactEvent {
}
interface ContactStatusChangedEvent extends ContactEvent {
oldStatus: Contact["status"];
newStatus: Contact["status"];
}
interface ContactEvents {
deleted: ContactDeletedEvent;
statusChanged: ContactStatusChangedEvent;
}
---
// T = "statusChanged", U = deleted | statusChanged | ContactStatusChangedEvent
// ContactStatusChangedEvent = ContactId | oldStatus | newStatus
function handleEvent<T, U extends keyof ContactEvents>(
eventName: T,
handler: (evt: ContactEvents[T]) => void
) {
if (eventName === "statusChanged") {
handler({ contactId: 1, oldStatus: "active", newStatus: "inactive"} )
}
}
handleEvent("statusChanged", evt => evt) // evt means Event
// Extended form
if (eventName === "statusChanged") {
ContactEvents.contactId => Contact["id"] => 1: // extended from
ContactEvents.oldStatus => Contact["status"] => ContactStatus => 'active'
ContactEvents.newStatus => Contact["status"] => ContactStatus => 'inactive'
}
// OR, shortenened
// Event => ContactEvents properties:
if ("statusChanged") {
ContactEvents.contactId: 1;
ContactEvents.oldStatus = 'active'
ContactEvents.newStatus = 'inactive'
}
```
### `any` types and `record` types
Using `any` is not very useful since it gets rid of all of the type checking features.
A useful alternative is `Record` which allows you to give properties multiple options instead.
```ts
// tells it that property must be a string OR number
let x: Record<string, string | number> = { name: "Wruce Bayne" }
x.number = 1234 // works
x.string = "cats" // works
x.log = () => console.log("nope") // doesn't work without Function
let y: Record<string, boolean | Function> = { name: True }
y.log = () => console.log("yes") // Works
```
`query` can help pass a value. The `query: Record<keyof Contact, Query>` here is setting the function to check for either a number or a string.
- If it finds it, it passes it to the `propertyQuery` variable == `[123, "Carol Weaver"]`
- If it matches the check, returns true
```ts
// query: Id, name, status, address, Query
function searchContacts(contacts: Contacts[], query: Record<keyof Contact, Query>) {
// go through contacts, filter contact
return contacts.filter(contact => {
// for id, name, status, address, Query
for (const property of Objects.keys(contact) as (keyof Contact)[]) {
// get the query object for this property
const propertyQuery = query[property]; //[123, Mike, active, 1609]
// check to see if it matches
// if [123,Mike,active,1609].matches(number,string) // true
if (propertyQuery && propertyQuery.matches(contact[property])) {
return true;
}
}
return false;
})
}
const filteredContacts = searchContacts(
[/* contacts */],
{
id: { matches: (id) => id === 123 },
name: { matches: (name) => name === "Carol Weaver" },
}
);
```
### Extending and Extracting Metadata from Existing Types
`Partial<>` copies the wrapped Record with all proporties defined as optional
```ts
type ContactQuery = Partial<Record<keyof Contact, Query>>
// Copies Contact Type but defines properties as optional
/*
type ContactQuery = {
id?: Query;
name?: Query;
status?: Query;
address?: Query;
}
*
```
`Omit<>` copies a type's definition, but allows you to omit properties from the clone definition.
```ts
type ContactQuery = Omit<Partial<Record<keyof Contact, Query>>, "address"|"status">
// Copies Contact Type but omits address property
/*
type ContactQuery = {
id?: Query;
name?: Query;
}
*
```
`Pick<>` is the opposite of `Omit<>` - it's second parameter allows me to choose which properties I want to display.
```ts
type ContactQuery = Partial<
Pick<
Record<keyof Contact, Query>,
"id"|"name"
>
>
// Copies Contact Type but picks properties to use
/*
type ContactQuery = {
id?: Query;
name?: Query;
}
*
```
`Required<>` is the opposite of the `Partial<>` helper, defining what properties are required.
```ts
type RequiredContactQuery = Required<ContactQuery>
// Copies ContactQuery and makes it's properties required
/*
type RequiredContactQuery = {
name: Query;
id: Query;
}
*
```
You can extract information by using a map syntax rather than simply copying. With a map, you can then access all of the types for each property mapped over in order to give more options when types are used in a function.
- otherwise, it would cause Query to force Any type
```ts
type ContactQuery = {
[TProp in keyof Contact]?: Query<Contact[TProp]>
}
/*
type ContactQuery = {
id?: Query<number>; // otherwise, it would cause Query to force Any type
name?: Query<string>;
status?: Query<ContactStatus>
address?: Query<Address>
email?: Query<string>
}
*/
function searchContacts(contacts: Contacts[], query: Record<keyof Contact, Query>) {
return contacts.filter(contact => {
for (const property of Objects.keys(contact) as (keyof Contact)[]) {
// add as Query here to tell it which property type to use
// Keeps us from defaulting to Any type
const propertyQuery = query[property] as Query<Contact[keyof Contact]>;
if (propertyQuery && propertyQuery.matches(contact[property])) {
return true;
}
}
return false;
})
}
```
### References
*see* [[⧋ Typescript|Typescript]]