# Working with types - Natalia ## 1.4 How to check if a string is part of an Enum? ### Using `in` operator to check if the key string is part of an Enum ```typescript enum BreakingBadCharacters { WalterWhite = "Heisenberg", JessePinkman = "Cap'n Cook", SaulGoodman = "Slippin' Jimmy", } const isCharacterInBreakingBad = (character: string): boolean => { return character in BreakingBadCharacters; }; console.log(isCharacterInBreakingBad("WalterWhite")); // true console.log(isCharacterInBreakingBad("GusFring")); // false ``` ### Using `Object.values()` to check if the value string is part of an Enum ```typescript enum BreakingBadNicknames { Heisenberg = "Walter White", CapnCook = "Jesse Pinkman", SlippinJimmy = "Saul Goodman", } const isNicknameFromBreakingBad = (nickname: string): boolean => { // let nicknames = Object.values(BreakingBadNicknames); let nicknames: string[] = Object.values(BreakingBadNicknames) as string[]; return nicknames.includes(nickname); }; console.log(isNicknameFromBreakingBad("Walter White")); // true console.log(isNicknameFromBreakingBad("Mike")); // false ``` ### Using `Object.values()` to check if both the key or value string is part of an Enum ```typescript enum BreakingBadElements { Methamphetamine = "Blue Crystal", HydrofluoricAcid = "Dissolve", Chromium = "Cr", } const isElementOrCompoundInBreakingBad = (input: string): boolean => { const keys = Object.keys(BreakingBadElements) as string[]; const values = Object.values(BreakingBadElements) as string[]; return keys.includes(input) || values.includes(input); }; console.log(isElementOrCompoundInBreakingBad("Methamphetamine")); // true (key) console.log(isElementOrCompoundInBreakingBad("Blue Crystal")); // true (value) console.log(isElementOrCompoundInBreakingBad("Pizza")); // false ``` ## 1.5. How to make only some properties of an interface mandatory? https://plainenglish.io/blog/how-to-make-certain-properties-optional-in-typescript-9b4f8e85c5de I feel like the way this blog develop the problem is really good. So I'll replicate those step here. ```typescript interface SpellCaster { name: string; surname: string; house: string; age: number; //Normal way to make a field mandatory pet?: Animal; //Normal way to make a field optional is using `?` } interface Animal { name: string; age: number; } //Assume we want to create a function to return the fullname const spellCasterFullName = ({ surname, name }: SpellCaster): string => { return `${name} ${surname}`; }; const spellCaster1: SpellCaster = { name: "Harry", surname: "Potter", house: "Gryffindor", age: 17, }; console.log(spellCasterFullName(spellCaster1.surname, spellCaster1.name)); ``` this works well, but now when you want to call the function spellCasterFullName else where, you would need to create a new SpellCaster `console.log(spellCasterFullName({ surname: "Granger", name: "Herminone" }));` this `{surname:"Granger", name: "Herminone"}` doesn't match SpellCaster type Then, it's where Partial<T> come to play: ```typescript function spellCasterFullName({ name, surname }: Partial<SpellCaster>): string { return `${name} ${surname}`; } console.log(spellCasterFullName({ surname: "Granger", name: "Herminone" })); ``` But then, `{surname:string, name: string}` become `{surname:string|undefined, name: string|undefined}` so we need to perfom some type check like we want name and surname mandatory and match with the type that define in the interface SpellCaster => we can use `Pick<T, Keys>` which selects some properties Keys from the T. ```typescript const spellCasterFullName = ({ name, surname, }: Pick<SpellCaster, "name" | "surname">): string => { return `${name} ${surname}`; }; console.log(spellCasterFullName(spellCaster1)); console.log(spellCasterFullName({ surname: "Granger", name: "Herminone" })); ``` ### A step further, in the case that we want to return characteristic of that person based on their house. it can be achieved by ```typescript const getCharacteristic = (house: string): string => { return "brave"; //let assume that all house characteristic return bravery, but we know that this value might differ from houses }; const spellCasterFullName = ({ name, surname, house }: SpellCaster): string => { if (house) { const characteristic = getCharacteristic(house); return `${name} ${surname} is ${characteristic}`; } return `${name} ${surname}`; }; console.log(spellCasterFullName(spellCaster1)); ``` but we don't need all SpellCaster properties just to return short name where there is no house then here is a solution ```typescript const spellCasterFullName = ( sp: Partial<SpellCaster> & Pick<SpellCaster, "name" | "surname">, ): string => { const { name, surname, house } = sp; if (house) { const characteristic = getCharacteristic(house); return `${name} ${surname} is ${characteristic}`; } return `${name} ${surname}`; }; console.log(spellCasterFullName(spellCaster1)); console.log(spellCasterFullName({ surname: "Granger", name: "Herminone" })); ``` ## 1.6 How to assign properties to an object dynamically? ```typescript //wrong way: const character = {} character.name = "Severus Snape"; //or const character = { name = "Severus Snape"; } character.age = 20 ``` ### 1. Declaring the Object with Explicit Type This method is used when the shape of your object is known and unlikely to change ```typescript const character: { name: string } = {}; character.name = "Severus Snape"; console.log("After dynamic assignment: ", character.name); ``` ### 2. Using the Index Signature of the Object Flexible object that can have any number of properties of a certain type ```typescript const magicalCreature: { [name: string]: any } = {}; // Dynamically adding more properties magicalCreature.species = "Snowy Owl"; magicalCreature.owner = "Harry Potter"; console.log(`Updated creature: ${magicalCreature.name}, a ${magicalCreature.species}, owned by ${magicalCreature.owner}`); //not even need to define the name property ``` ### 3. Using the Record Utility Type ```typescript const houseColors: Record<string, string[]> = { Gryffindor: ["Scarlet", "Gold"], }; console.log("Before dynamic assignment: ", houseColors.Gryffindor); // Dynamically adding more houses and their colors houseColors.Hufflepuff = ["Yellow", "Black"]; houseColors.Ravenclaw = ["Blue", "Bronze"]; houseColors.Slytherin = ["Green", "Silver"]; console.log("After dynamic assignment: "); for (const house in houseColors) { console.log(`${house}: ${houseColors[house].join(" and ")}`); } ``` ## 1.7 How to exclude properties readonly from a type Original Character Type ```typescript type Wizard = { readonly id: number; readonly name: string; house: string; }; ``` ### 1. Making All Properties Mutable ```typescript type RemoveReadonlyForAll<T> = { -readonly [P in keyof T]: T[P]; }; type EditableWizard = RemoveReadonlyForAll<Wizard>; const editableWizard: EditableWizard = { id: 1, name: "Harry Potter", house: "Gryffindor" }; // Demonstrating mutability editableWizard.id = 2; // Success editableWizard.name = "Harry James Potter"; // Success editableWizard.house = "Slytherin"; // Success ``` ### 2. Removing readonly from Specific Properties ```typescript type RemoveReadonly<T, K extends keyof T> = { -readonly [P in K]: T[P]; } & { [P in Exclude<keyof T, K>]: T[P]; }; // Making only the 'name' property mutable type NameChangeableWizard = RemoveReadonly<Wizard, 'name'>; const nameChangeableWizard: NameChangeableWizard = { id: 3, name: "Hermione Granger", house: "Gryffindor" }; // Trying to mutate // nameChangeableWizard.id = 4; // Error: Cannot assign to 'id' because it is a read-only property. nameChangeableWizard.name = "Luna Lovegood"; // Success // nameChangeableWizard.house = "Ravenclaw"; // This would still work as 'house' was mutable from the start. ``` ### 3. Manually Adjusting Properties ```typescript // Exclude 'id' and 'name' from Wizard, then add them back as mutable type IdentityChangedWizard = Omit<Wizard, 'id' | 'name'> & { id: number; name: string; }; const identityChangedWizard: IdentityChangedWizard = { id: 5, name: "Ron Weasley", house: "Gryffindor" }; // Demonstrating allowed changes identityChangedWizard.id = 6; // Success identityChangedWizard.name = "Ronald Bilius Weasley"; // Success // identityChangedWizard.house = "Hufflepuff"; // This would still work as 'house' was mutable from the start. ``` ## 1.8 How to extract a type from a interface property? ```typescript interface MagicalBook { title: string; author: string; magicalProperties: any; } interface HogwartsCurriculum { potions: { textbook: MagicalBook, supplementary: MagicalBook[] }; defenseAgainstTheDarkArts: { textbook: MagicalBook, supplementary: MagicalBook[] }; // Additional classes can be added here } ``` ### Extract the specific structure using indexed access type ```typescript // Extracting the type for supplymentary book of potion class type PotionClassSupplymentaryBook = HogwartsCurriculum["potions"]["supplymentary"] //PotionClassSupplymentaryBook = MagicalBook[] // Extracting the type for Potions class reading materials type PotionsReadingMaterials = HogwartsCurriculum["potions"]; // PotionsReadingMaterials = { // textbook: MagicalBook, // supplementary: MagicalBook[] // } ``` Question: why using `type PotionClassSupplymentaryBook = HogwartsCurriculum["potions"]["supplymentary"] //PotionClassSupplymentaryBoo` but not `type PotionClassSupplymentaryBook = MagicalBook[]`? In a scenario where the *structure of MagicalBook changes, or the potions class structure is updated to include a new category of books*. If you had used `MagicalBook[]` directly, these changes would not be reflected in the `PotionClassSupplementaryBook` type without manual intervention. However, by using `HogwartsCurriculum["potions"]["supplementary"]`, your type definitions remain accurate and **up-to-date** with minimal effort. ### Utilizing keyof to Enumerate Hogwarts Classes ```typescript type HogwartsClass = keyof HogwartsCurriculum; //HogwartsClass = "potions" | "defenseAgainstTheDarkArts" ``` - Example Usage We can use this HogwartsClass type to ensure that functions dealing with classes only accept valid class names. ```typescript function listMaterialsForClass(className: HogwartsClass) { // Implementation: list all materials for the given class } ``` ## 1.9 How to restrict a number to a certain range ```typescript type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N ? Acc[number] : Enumerate<N, [...Acc, Acc['length']]>; type Range<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>>; type T = Range<3,5>; ``` ### Goal We want to create a type that represents a sequence of numbers from the range 3-5 . This sequence is a range of numbers from 3 (inclusive) to 5 (exclusive). ### Enumerate Type First, we have a helper type called Enumerate. This generates a sequence of numbers starting from 0 up to (but not including) a number N you specify. ```typescript type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N ? Acc[number] : Enumerate<N, [...Acc, Acc['length']]> //Enumerate<3> //=> This would generate a union from 0 to 2: 0|1|2 ``` Here's how it works: - It takes a number N and starts with an empty list (or array) called Acc (short for "Accumulator"). - It looks at the length of `Acc`. If the length matches N, it stops and gives us a list of numbers from 0 up to N-1. - If the length of Acc is not yet N, it adds the current length of Acc to the end of Acc and tries again. This step is repeated until Acc contains N numbers. #### Dry Run Example: Enumerate<3> Let's see how this works step by step to generate a sequence [0, 1, 2]. ```typescript type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N ? Acc[number] : Enumerate<N, [...Acc, Acc['length']]> //Initial Call: Enumerate<3> //Step 1: Start with an empty Acc //Enumerate<3> Acc = [] Acc.length = 0 //(Does not match N=3) //=> Acc['length'] extends 3 return false //=> call Enumerate<N, [...Acc, Acc['length']]> // that now is Enumerate<3, [0]> //Step 2: Add Acc.length to Acc and recurse //Enumerate<3, [0]> New Acc = [0] Acc.length = 1 //(Does not match N=3) //Recursive Call Enumerate<3, [0,1]> //Step 3: Add Acc.length to Acc and recurse //Enumerate<3, [0,1]> New Acc = [0,1] Acc.length = 2 //(Does not match N=3) //Recursive Call Enumerate<3, [0,1,2]> //Step 4: Add Acc.length to Acc and recurse //Enumerate<3, [0,1,2]> New Acc = [0,1,2] Acc.length = 3 //(match N=3) //=> Acc['length'] extends 3 return true //so we stop here and return Acc["number"] ``` Final Result The process stops because Acc.length now matches N. The resulting type from `Acc[number]` is `0 | 1 | 2`, representing each number in the sequence as a union of literals. ### Range Type ```typescript type Range<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>>; ``` How It Works: - `Exclude<Enumerate<T>, Enumerate<F>>`: This uses TypeScript's `Exclude` utility type to subtract the numbers from `0` to `F-1` (generated by `Enumerate<F>`) from the numbers `0` to `T-1` (generated by `Enumerate<T>`). The result is a type that represents each number between `F` and `T-1` as a literal. Example: ```typescript type T = Range<3, 5>; ``` this equal to ```typescript Exclude<Enumerate<5>, Enumerate<3>> ``` `Enumerate<5>` generates a union type `0 | 1 | 2 | 3 | 4`. `Enumerate<3>` generates a union type `0 | 1 | 2`. => `Exclude< 0|1|2|3|4 , 0|1|2> ` => result in new union type: `3 | 4` => `type T = 3 | 4` ### Conclusion ```typescript type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N ? Acc[number] : Enumerate<N, [...Acc, Acc['length']]>; type Range<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>>; type T = Range<3,5>; //=> type T = 3 | 4 ``` ## 1.10 How to Check if an Object Implements a Type or Interface? **By using "User Defined Type Guard"** Imagine we're at Hogwarts, and we need to verify if a particular magical device, let's call it a `SpellChecker`, implements the `ISpell` interface. This interface ensures that every magical device can cast a spell (castSpell) and has a spell name (spellName). First, we define the ISpell interface ```typescript interface ISpell { spellName: string; castSpell: () => void; } ``` To check if a magical device is truly capable of casting spells as per the ISpell interface, we conjure a **"User Defined Type Guard"** function. ```typescript const implementsISpell = (object: any): object is ISpell =>{ return object && typeof object.spellName === 'string' && typeof object.castSpell === 'function'; } ``` We now have a magical device that claims to be capable of checking spells: ```typescript const spellChecker = { spellName: "Lumos", castSpell: () => console.log("Let there be light"), }; //Implement type checking at run time if (implementsISpell(spellChecker)) { console.log("spellChecker implements ISpell"); } else { console.log("spellChecker does not implement ISpell"); } ``` ### Pros and Cons **Pros** - Type Safety: This method ensures that objects adhere to specific structures at runtime, offering a layer of type safety beyond compile-time checks. - Flexibility: Type guards can be customized to check for various properties and methods, providing flexibility in validation logic. **Cons** - Manual Checks: For objects with many properties, writing and updating type guards can become cumbersome. - Runtime Overhead: These checks occur at runtime, potentially impacting performance if used excessively. ### Use Cases - Input Validation: Ensuring that dynamic content, such as user input or API responses, matches expected types and structures. - Interoperability: Verifying that objects from external libraries or modules meet the required interfaces for integration. - Debugging: Aiding in debugging by catching type-related errors at runtime, providing an additional layer of error checking. ## 1.11 Keeping the relation key-value using Objects.entries Define a WizardInventory type ```typescript type WizardInventory = { wand: number, potionName: string }; ``` The `KeyValuePairs` mapped type transforms an object type into an array of `[key, value]` tuples, keeping the association between each key and its corresponding value type. ```typescript type KeyValuePairs<T> = { [P in keyof T]: [P, T[P]]; }[keyof T][]; ``` **Applying KeyValuePairs to WizardInventory** ```typescript KeyValuePairs<WizardInventory> ``` When we apply `KeyValuePairs<T>` to `WizardInventory`, here's how TypeScript processes the type: **1. Iterating Over Keys with `[P in keyof T]`** ```typescript keyof WizardInventory //result in type "wand" | "potionName" //the mapped type [P in keyof T] iterates over each of these keys [P in keyof T] //loop over "wand" and "potitionName" ``` **2. Creating Tuples with [P, T[P]]** On each iteration, TypeScript creates a tuple [P, T[P]]: - For `P = "wand"`, `T[P]` is number. - The tuple becomes `["wand", number]`. - For `P = "potionName"`, `T[P]` is string. - The tuple becomes `["potionName", string]`. - At this point, we have a mapped type where each key from WizardInventory is mapped to a tuple of the key name and its type: ```typescript { wand: ["wand", number], potionName: ["potionName", string] } ``` **3. Converting to a Union of Tuples with [keyof T][]** Applying [keyof T] on this mapped type, which extracts the types of each property into a union. Result in: ```typescript ["wand", number] | ["potionName", string] ``` Finally, adding [] at the end constructs an array type from this union: ```typescript (["wand", number] | ["potionName", string])[] ``` Final result: ```typescript type WizardInventory = { wand: number, potionName: string }; type KeyValuePairs<T> = { [P in keyof T]: [P, T[P]]; }[keyof T][]; type InventoryEntries = KeyValuePairs<WizardInventory>; // Type is (["wand", number] | ["potionName", string])[] ``` other example: ```typescript type KeyValuePairs<T> = { [P in keyof T]: [P, T[P]]; }[keyof T][]; //Example 1: type EventHandlers = { onClick: () => void; onMouseEnter: () => void; onKeyPress: (key: string) => boolean; }; type HandlerEntries = KeyValuePairs<EventHandlers>; //type is ["onClick", () => void] | ["onMouseEnter", () => void] | ["onKeyPress", (key: string) => boolean])[] type MixedTypesObject = { id: number; isValid: boolean; name: string; computeScore: (values: number[]) => number; }; type MixedEntries = KeyValuePairs<MixedTypesObject>; // Type is [("id", number) | ("isValid", boolean) | ("name", string) | ("computeScore", (values: number[]) => number)][] ```