diff --git a/Programming_Languages/TypeScript/Function_Overloads.md b/Programming_Languages/TypeScript/Function_overloads.md similarity index 100% rename from Programming_Languages/TypeScript/Function_Overloads.md rename to Programming_Languages/TypeScript/Function_overloads.md diff --git a/Programming_Languages/TypeScript/Generics.md b/Programming_Languages/TypeScript/Generics.md new file mode 100644 index 0000000..e61365a --- /dev/null +++ b/Programming_Languages/TypeScript/Generics.md @@ -0,0 +1,206 @@ +--- +tags: + - Programming_Languages + - typescript +--- + +# Generics + +**Generics allow you to write code that is type-safe yet flexible enough to accommodate specifics that you are not able to ascertain in advance.** + +Generics can help promote: + +- type consistency when specific types cannot be known in advance +- reduced repetition when writing functions and methods +- extensibility and future-proofing + +## First example: simplest use case + +In the code below we have a simple JavaScript function that receives a value and an an array as parameters. It returns a new array comprising the original array plus the additional value: + +```js +function generateArray(existingArray, newValue) { + return [...existingArray, newValue]; +} +``` + +Imagine that we want to ensure that each of the parameters share the same data type. In other words: if the function is passed a string array, the second parameter must also be a string. For example, it should not be the case that you can append a string to an array of numbers. + +Now imagine that we don't know in advance what type the value or array will be, we just know that the data types of the parameters must match. + +In converting the function to TypeScript, one way of overcoming our lack of foreknowledge would be to deploy `any`. This way it doesn't matter which types are passed to the function: + +```ts +function generateArray(existingArray: any[], newValue: any): any[] { + return [...existingArray, newValue]; +} +``` + +But this is no solution at all. The problem — as always with `any` — is that it strips our function of any type checks whatsoever and would therefore invite calls of form: `generateArray([1,2,3], 'lorem')`. + +Enter generics: + +```ts +function generateArray(existingArr: T[], newValue: T): T[] { + return [...existingArr, newValue]; +} +``` + +Now, whilst we haven't asserted ahead of time which types will be used, whichever types we do pass in, must match. The function header is saying: + +- both arguments must be of the same type (represented by `T`) +- the function will return an array of this same `T` type. + +If I then tried to run the function with unmatched types (for example `generateArray([1,2,3,4], true)` ) TypeScript would raise the following error: + +``` +Argument of type 'boolean' is not assignable to parameter of type 'number' +``` + +Note that even though the function in question does not express any preference for number types, given that our first parameter is a number, TypeScript knows that the second parameter must also be a number. + +> In the generic function we have used `T` as our placeholder for a generic type as this is the convention. However there is no compunction to do so. We could have used any letter or string, providing that the string is not a reserved term. + +## Second example + +This example extends the first and demonstrates how generics can help to reduce repetitive code whilst also providing type safety. + +Imagine we have a single object that can be classified against multiple data sets. For instance a book may be translated into different languages, be available in a variety of formats, and be published in multiple countries. Let's create custom types reflecting this: + +```ts +type Translations = { + [english: string]: string; + german: string; + french: string; +}; + +type Publishers = { + [usa: string]: string; + uk: string; + germany: string; +}; + +type Formats = { + [hardback: string]: boolean; + paperback: boolean; + audio: boolean; +}; +``` + +Now let's say we have a book _Dune_ that is a database entry corresponding to the `Translations` type + +```ts +const dune: Translations = { + english: 'https://www.amazon.com/dune', + german: 'https://www.amazon.de/dune', + french: 'https://www.amazon.fr/dune', +}; +``` + +Our users want to be able to quickly check different properties of a given book in the database. To allow them to check for specific translations we might write a utility function: + +```ts +function isTranslationAvailable( + database: Translations, + translation: string | number, +): translation is keyof Translations { + return translation in database; +} +``` + +We would expect `isTranslationAvailable(dune, 'german')` to return `true` and `isTranslationAvailable(dune, 'hebrew')` to return `false`. + +Next we want to see if _Dune_ is available as audio book, we could adapt `isTranslationAvailable` : + +```tsx +function isFormatAvailable(database: Formats, format: string | number): format is keyof Formats { + return format in database; +} +``` + +This is clearly sub-optimal: we require a different lookup function for every property type yet the logic is identical. This is unnecessarily verbose and insufficiently abstracted. It is also not very adaptable in that for each new property set we need to create a different function. We can imagine that if the list of possible properties were to grow over time, additional code would need to be written and existing references updated. + +The solution then, is to genericise the utility function so tht + +## Another example + +--- + +This example demonstrates how we can use generics to reduce repetition when writing functions and is also a more realistic use case. + +Let's say we have two types or interfaces: + +```tsx +type VideoFormatUrls = { + format720p: URL; + format1080p: URL; +}; +``` + +```tsx +type SubtitleFormatUrls = { + english: URL; + german: URL; +}; +``` + + + +```tsx +let m = 'https://developer.mozilla.org'; +let a = new URL('/', m); +``` + +An example of an object matching these type definitions: + +```tsx +const videoFormats: VideoFormatUrls = { + format720p: https://www.format720p.co.uk, + format1080p: https://www.format1080p.co.uk +} +``` + +Imagine we wanted to be able to check whether a given film is available in a certain video format. We could write a function like this: + +```tsx +function isFormatAvailable(obj: VideoFormatUrls, format: string): format is keyof VideoFormatUrls { + return format in obj; +} +``` + + + +Now imagine that we need to do the same thing with subtitles, but given that `isFormatAvailable()` is typed to the `VideoFormatUrls` type we would get an error if we used this function for subtitles. But we also don't want to write a near identical function typed to `SubtitleFormatUrls` to subtitles just to ensure adequate type safety. + +Alternatively we could use a union type, for example: + +```tsx +function isFormatAvailable( + obj: VideoFormatUrls | SubtitleFormatUrls, + format: string, +): format is keyof VideoFormatUrls { + return format in obj; +} +``` + +But this quickly becomes unwieldy if we, for the sake of argument have a great many URL types that we want the function to utilise. + +This is where generics become super helpful. Here is how we would rewrite the function as a generic: + +```tsx +function isAvailable(obj: Formats, key: string): key is keyof Formats { + return key in obj; +} +``` + +We could then explicitly type our calls of this function, viz: + +```tsx +isFormatAvailable(subtitles, 'english'); +``` diff --git a/Programming_Languages/TypeScript/Intersection_types.md b/Programming_Languages/TypeScript/Intersection_types.md new file mode 100644 index 0000000..1f80f5a --- /dev/null +++ b/Programming_Languages/TypeScript/Intersection_types.md @@ -0,0 +1,73 @@ +--- +tags: + - Programming_Languages + - typescript +--- + +# Intersection types + +Like union types, intersection types allow us to combine the properties of existing primitive and custom types to make composites. + +### Creating an intersection + +Say you have the following three types: + +```ts +type IConcert = { + date: number; + location: string; + band: string; +}; + +type IConference = { + date: number; + location: string; + keynoteSpeaker: string; +}; + +type IMeeting = { + date: number; + location: string; + agenda: string[]; +}; +``` + +Each type corresponds to a different event and has two properties in common with the other events: `date` and `location`. + +Rather than repeat these properties for each individual type, we could create a general `IEvent` type and apply this as an intersection to each of the individual event types: + +```ts +// The common event type +type IEvent = { + date: number; + location: string; +}; +``` + +```ts +// The intersection + +type IConcert = IEvent & { + band: string; +}; + +type IConference = IEvent & { + keynoteSpeaker: string; +}; +``` + +> In the case of interfaces, instead of using `&` we would write `interface IConcert extends IEvent` + +### Alternative method + +There is another equivalent way of intersecting the types. We could first define `IConcert` and `IConference` as their own types (without `extends` or `&`) and then intersect with, e.g., type IConcert = SelfStandingConcertType & IEvent. + +### Benefits of intersections + +- It is more concise and we do not repeat ourselves needlessly +- Common properties can be modelled **in one place** which makes updates and changes much easier: we do not have to change multiple type declarations, just change the 'master' type that is intersected with the variants. +- It is more readable: we can see which properties are unique to the type without having to parse repeated properties. + +## Set theory: understanding errors + +With intersections we literally use the & operator which discloses that the logic is equivalent to logical AND: a member of the intersection must have the properties of both types. In the case of primitive values this is impossible or, more accurately, it results in an empty set because, for example, `string` and `boolean` do not share any common values. This means you will get an error if you assign an intersection type to a value that does not have all the features of each custom type in the intersection. diff --git a/Programming_Languages/TypeScript/Non_null_assertion.md b/Programming_Languages/TypeScript/Non_null_assertion.md new file mode 100644 index 0000000..bfa7328 --- /dev/null +++ b/Programming_Languages/TypeScript/Non_null_assertion.md @@ -0,0 +1,27 @@ +--- +tags: + - Programming_Languages + - typescript +--- + +# Non-null assertion + +Oftentimes you will encounter errors of the form: + +`Property [property_name] does not exist on type [type_name].ts(2339)` + +This means the interpreter is not able to determine whether the property which you are seeking to change exists. It can't be sure that you are not trying to update a property that is either `null` or `undefined`. This is good type checking but it means that in cases when _you know_ the property either exists or _will_ exist at runtime, you are going to face a code-breaking error. + +To get around this you can use non-null assertion. Like the name suggests, here you are saying to TypeScript: thanks but I am sure this property exists. When you do this you get the value you are trying to change with `null` and `undefined` excluded from it. + +This typically occurs when you are assigning a property to a value and TS doesn't know whether the value will exist or be accessible at this point in the runtime. A good scenario would be dynamic data passed into a function: + +```ts +function doSomethingWith(typeAlias: ITypeAlias) { + let x = typeAlias!.somePropertyEvidentAtRuntime; +} +``` + +However it should be used carefully and sparingly because you are obviously turning off core type-checking and overuse nullifies the purpose of TypeScript. + +One way to get around it is to use better [type-guarding](./Type_guarding_and_narrowing.md) and conditionality and to cover cases where the value may be undefined. diff --git a/Programming_Languages/TypeScript/Setup_and_configuration.md b/Programming_Languages/TypeScript/Setup_configuration.md similarity index 100% rename from Programming_Languages/TypeScript/Setup_and_configuration.md rename to Programming_Languages/TypeScript/Setup_configuration.md diff --git a/Programming_Languages/TypeScript/Type_guarding_and_narrowing.md b/Programming_Languages/TypeScript/Type_guarding_narrowing.md similarity index 100% rename from Programming_Languages/TypeScript/Type_guarding_and_narrowing.md rename to Programming_Languages/TypeScript/Type_guarding_narrowing.md diff --git a/Programming_Languages/TypeScript/Union_types.md b/Programming_Languages/TypeScript/Union_types.md new file mode 100644 index 0000000..fb9574d --- /dev/null +++ b/Programming_Languages/TypeScript/Union_types.md @@ -0,0 +1,88 @@ +--- +tags: + - Programming_Languages + - typescript +--- + +# Union types + +Like intersection types, union types allow us to combine the properties of existing primitive and custom types to make composites. + +Intersection types work well when you have complete knowledge of what form your data will take. This approach only works if you know in advance that your data will be and that it will share properties from more than one type. But what if you don't have this knowledge? You only know that it will be one of the three possible types? This is where union types become helpful. Instead of saying that event X will be of type A we say that event X may be one of types A or B or C. + +> A union type is a type formed from two or more other types, representing values that may be any one of those types. We refer to each of these types as the union’s members. + +## Demonstration + +```ts +type Petrol = { + maxSpeed: number; + tankVolume: number; // unique property +}; + +type Electric = { + maxSpeed: number; + maxCharge: number; // unique property +}; + +type Car = Petrol | Electric; + +const tesla: Car = { + maxSpeed: 120, + maxCharge: 22, +}; +``` + +- We have defined two custom types: `Petrol` and `Electric` +- We have unified them to make a new custom type of `Car` +- `tesla` is an instance of the union type + +## Usage with functions + +Union type syntax is not limited to the declaration phase. You can also use them as parameters to functions or function return types: + +```ts +function generateCar(car: Petrol | Electric): void { + console.log(`The car has a max speed of ${car.maxSpeed}`); +} +``` + +## Unifying primitive and custom types + +We can also use unions with primitive types as well as custom types: + +```ts +function printId(id: number | string): void { + console.log(`Your id is ${id}`); +} +``` + +We can also combine primitive and custom types in the same union: + +```ts +function printStringOrNumber(): string | Petrol {} +``` + +## Set theory: understanding errors + +In JavaScript we have the primitive types of string , number , boolean etc. In TS, these types are distinct and exclusive: if you declare that x : string you cannot redefine it as boolean or expect Boolean properties and methods to belong to a string type. For instance a string type is incapable of having as its value false. + +This is not true in the case of unions. + +With a union of `string | boolean` a value `x` could be `'banana'` or `true`. We are allowing for all values that are either from the set string or the set boolean. Logically this is equivalent to the OR operator: a value of type `string | boolean` could be one of infinite possible strings or true/false. With union types then, we radically widen the scope of possible values for the type. a + +These constraints don't just apply to the values themselves but the methods and properties you can use with the values. For instance a boolean doesn't have the property of `length`. Therefore if you have a union type of `string|boolean` you are going to get an error if you apply `length` to it since **the property must exist on both types that comprise the union**. The way round this is to use type narrowing and execute control flow based on the parameter that is passed to a function that is declared to be of this mixed type. + +Here is an example of this: + +```ts +function printId(id: number | string) { + if (typeof id === 'string') { + // In this branch, id is of type 'string' + console.log(id.toUpperCase()); + } else { + // Here, id is of type 'number' + console.log(id); + } +} +```