## 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]]