From fe24de87e9ae20a2337deef343c6521e5319a731 Mon Sep 17 00:00:00 2001 From: khamidullo Date: Thu, 7 Dec 2023 17:38:24 +0500 Subject: [PATCH] begginer is done --- README.md | 20 +++- .../default-generic-arguments/metadata.json | 13 ++ .../default-generic-arguments/prompt.md | 64 ++++++++++ .../default-generic-arguments/solutions/1.ts | 8 ++ challenges/default-generic-arguments/tests.ts | 17 +++ .../default-generic-arguments/tsconfig.json | 7 ++ challenges/default-generic-arguments/user.ts | 8 ++ .../generic-function-arguments/metadata.json | 8 ++ .../generic-function-arguments/prompt.md | 76 ++++++++++++ .../generic-function-arguments/solutions/1.ts | 3 + .../generic-function-arguments/tests.ts | 58 +++++++++ .../generic-function-arguments/tsconfig.json | 7 ++ challenges/generic-function-arguments/user.ts | 3 + challenges/indexed-types/metadata.json | 8 ++ challenges/indexed-types/prompt.md | 97 +++++++++++++++ challenges/indexed-types/solutions/1.ts | 2 + challenges/indexed-types/tests.ts | 22 ++++ challenges/indexed-types/tsconfig.json | 7 ++ challenges/indexed-types/user.ts | 2 + challenges/mapped-object-types/metadata.json | 8 ++ challenges/mapped-object-types/prompt.md | 79 ++++++++++++ challenges/mapped-object-types/solutions/1.ts | 7 ++ challenges/mapped-object-types/tests.ts | 112 ++++++++++++++++++ challenges/mapped-object-types/tsconfig.json | 7 ++ challenges/mapped-object-types/user.ts | 7 ++ challenges/primitive-data-types/user.ts | 16 +-- challenges/type-aliases/user.ts | 10 +- challenges/typeof/metadata.json | 8 ++ challenges/typeof/prompt.md | 91 ++++++++++++++ challenges/typeof/solutions/1.ts | 6 + challenges/typeof/tests.ts | 100 ++++++++++++++++ challenges/typeof/tsconfig.json | 7 ++ challenges/typeof/user.ts | 6 + 33 files changed, 876 insertions(+), 18 deletions(-) create mode 100644 challenges/default-generic-arguments/metadata.json create mode 100644 challenges/default-generic-arguments/prompt.md create mode 100644 challenges/default-generic-arguments/solutions/1.ts create mode 100644 challenges/default-generic-arguments/tests.ts create mode 100644 challenges/default-generic-arguments/tsconfig.json create mode 100644 challenges/default-generic-arguments/user.ts create mode 100644 challenges/generic-function-arguments/metadata.json create mode 100644 challenges/generic-function-arguments/prompt.md create mode 100644 challenges/generic-function-arguments/solutions/1.ts create mode 100644 challenges/generic-function-arguments/tests.ts create mode 100644 challenges/generic-function-arguments/tsconfig.json create mode 100644 challenges/generic-function-arguments/user.ts create mode 100644 challenges/indexed-types/metadata.json create mode 100644 challenges/indexed-types/prompt.md create mode 100644 challenges/indexed-types/solutions/1.ts create mode 100644 challenges/indexed-types/tests.ts create mode 100644 challenges/indexed-types/tsconfig.json create mode 100644 challenges/indexed-types/user.ts create mode 100644 challenges/mapped-object-types/metadata.json create mode 100644 challenges/mapped-object-types/prompt.md create mode 100644 challenges/mapped-object-types/solutions/1.ts create mode 100644 challenges/mapped-object-types/tests.ts create mode 100644 challenges/mapped-object-types/tsconfig.json create mode 100644 challenges/mapped-object-types/user.ts create mode 100644 challenges/typeof/metadata.json create mode 100644 challenges/typeof/prompt.md create mode 100644 challenges/typeof/solutions/1.ts create mode 100644 challenges/typeof/tests.ts create mode 100644 challenges/typeof/tsconfig.json create mode 100644 challenges/typeof/user.ts diff --git a/README.md b/README.md index 3553f69..7408a56 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,21 @@ --- -# TypeScript Foundations +# Typescript Foundations > TypeScript Foundations is a curated set of challenges designed to build a strong foundation. From basic syntax to advanced concepts, this collection offers hands-on exercises to help you become a TypeScript Hero -## Beginer - -- [Primitive Data Types](https://github.com/xam1dullo/typehero/blob/main/challenges/primitive-data-types/prompt.md) , 2023-11-22 -- [Type Aliases](https://github.com/xam1dullo/typehero/blob/main/challenges/type-aliases/prompt.md), 2023-11-23 +## Beginner + +- [Primitive Data Types](https://github.com/xam1dullo/typehero/blob/main/challenges/primitive-data-types/prompt.md), 2023-11-22 -> [Solution](https://github.com/xam1dullo/typehero/blob/main/challenges/primitive-data-types/user.ts) +- [Type Aliases](https://github.com/xam1dullo/typehero/blob/main/challenges/type-aliases/prompt.md) , 2023-11-23 -> [Solution](https://github.com/xam1dullo/typehero/blob/main/challenges/type-aliases/user.ts) +- [Literal Types](https://github.com/xam1dullo/typehero/blob/main/challenges/literal-types/prompt.md) , 2023-11-24 -> [Solution](https://github.com/xam1dullo/typehero/blob/main/challenges/index-signatures/user.ts) +- [Index Signatures](https://github.com/xam1dullo/typehero/blob/main/challenges/index-signatures/prompt.md) , 2023-11-25 -> [Solution](https://github.com/xam1dullo/typehero/blob/main/challenges//user.ts) +- [The `typeof` Operator](https://github.com/xam1dullo/typehero/blob/main/challenges/typeof/prompt.md) , 2023-11-26 -> [Solution](https://github.com/xam1dullo/typehero/blob/main/challenges/typeof/user.ts) +- [Indexed Types](https://github.com/xam1dullo/typehero/blob/main/challenges/indexed-types/prompt.md) , 2023-11-27 -> [Solution](https://github.com/xam1dullo/typehero/blob/main/challenges/indexed-types/user.ts) +- [The `keyof` Operator](https://github.com/xam1dullo/typehero/blob/main/challenges/keyof/prompt.md) , 2023-11-29 -> [Solution](https://github.com/xam1dullo/typehero/blob/main/challenges/keyof/user.ts) +- [Generic Type Arguments](https://github.com/xam1dullo/typehero/blob/main/challenges/generic-type-arguments/prompt.md) , 2023-11-30 -> [Solution](https://github.com/xam1dullo/typehero/blob/main/challenges/generic-type-arguments/user.ts) +- [Generic Type Constraints](https://github.com/xam1dullo/typehero/blob/main/challenges/generic-type-constraints/prompt.md) , 2023-12-01 -> [Solution](https://github.com/xam1dullo/typehero/blob/main/challenges//user.ts) +- [Generic Type Constraints](https://github.com/xam1dullo/typehero/blob/main/challenges/mapped-object-types/prompt.md) , 2023-12-02 -> [Solution](https://github.com/xam1dullo/typehero/blob/main/challenges/mapped-object-types/user.ts) +- [Generic Function Arguments](https://github.com/xam1dullo/typehero/blob/main/challenges/generic-function-arguments/prompt.md) , 2023-12-03 -> [Solution](https://github.com/xam1dullo/typehero/blob/main/challenges/mapped-object-types/user.ts) +- [Default Generic Arguments](https://github.com/xam1dullo/typehero/blob/main/challenges/default-generic-arguments/prompt.md) , 2023-12-04 -> [Solution](https://github.com/xam1dullo/typehero/blob/main/challenges/default-generic-arguments/user.ts) diff --git a/challenges/default-generic-arguments/metadata.json b/challenges/default-generic-arguments/metadata.json new file mode 100644 index 0000000..d34ac88 --- /dev/null +++ b/challenges/default-generic-arguments/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "default-generic-arguments", + "label": "Default Generic Arguments", + "description": "Default generic arguments allow you to optionally pass a generic argument.", + "difficulty": "beginner", + "prerequisites": [ + "generic-type-arguments", + "type-unions", + "generic-type-constraints", + "indexed-types" + ], + "author": "dimitropoulos" +} diff --git a/challenges/default-generic-arguments/prompt.md b/challenges/default-generic-arguments/prompt.md new file mode 100644 index 0000000..c5a4fb9 --- /dev/null +++ b/challenges/default-generic-arguments/prompt.md @@ -0,0 +1,64 @@ +## Why Generic Arguments Need Defaults + +Technically speaking, no program absolutely requires default arguments. It's a convenience feature for the humans writing programs. + +Sometimes you have a value in mind for an argument and you don't want to force users of your library to constantly provide the same value over and over again. That's the situation where default arguments come in handy. + +## How To Create Default Type Arguments + +### First: A Refresher On Function Defaults + +Let's take the case of a run-of-the-mill logging function. The logger always needs a message to log, but [log level](https://en.wikipedia.org/wiki/Syslog#Severity_level) might be something you want to be optional: + +```ts +type LogLevel = 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical'; + +const log = (message: string, level: LogLevel = 'info') => { + // application logic +}; + +log('this has an explicit debug log level', 'debug'); +log('this has an implicit info debug level'); +``` + +You can do exactly this kind of thing for generic types! This is yet another parallel between functions and generic types! + +### Default Generic Arguments + +```ts +type Log = { + message: Message; + level: Level; +}; + +type ExplicitDebugLog = Log<'explicit debug', 'debug'>; +type ImplicitInfo = Log<'implicit info'>; +``` + +Doesn't that look super similar!? A lot of the same rules apply in types that apply to function argument defaults. For example, you can't have a required argument following a default argument. + +One notable exception is that there's no TypeScript value you can pass that will work like `undefined` does for JavaScript default arguments: + +```ts +const greet = (name = 'Stranger') => { + console.log(`Hello ${name}!`); +}; + +greet(); // Hello Stranger! +greet(undefined); // Hello Stranger! +greet('Mr. Monkey'); // Hello Mr. Monkey! +``` + +In TypeScript, even if you provide `never` or `unknown` or `any`, the value will be inserted instead of the default. + +## Solving This Challenge + +Your goal is to create some types that have have the correct default argument. + +The second type, `TSConfig`, should feel a bit difficult, but if you get stuck, feel free to check out the little hint below: + +### small hint + +1. Start with a literal object as the parameter default. +1. Use [indexed types](https://typehero.dev/challenge/indexed-types). +1. The error you'll see is because of a missing [generic constraint](https://typehero.dev/challenge/generic-type-constraints). diff --git a/challenges/default-generic-arguments/solutions/1.ts b/challenges/default-generic-arguments/solutions/1.ts new file mode 100644 index 0000000..973f3db --- /dev/null +++ b/challenges/default-generic-arguments/solutions/1.ts @@ -0,0 +1,8 @@ +type ApiRequest = { + data: Data; + method: Method; +}; + +type TSConfig = { + strict: Config['strict']; +}; diff --git a/challenges/default-generic-arguments/tests.ts b/challenges/default-generic-arguments/tests.ts new file mode 100644 index 0000000..a63deed --- /dev/null +++ b/challenges/default-generic-arguments/tests.ts @@ -0,0 +1,17 @@ +import { Expect, Equal } from 'type-testing'; + +type test_ApiRequest_explicitPost = Expect< + Equal, { data: string; method: 'POST' }> +>; + +type test_ApiRequest_implicitGet = Expect< + Equal, { data: number; method: 'GET' }> +>; + +type test_TSConfig_default = Expect>; + +type test_TSConfig_true = Expect, { strict: true }>>; + +type test_TSConfig_false = Expect, { strict: false }>>; + +type test_TSConfig_boolean = Expect, { strict: boolean }>>; diff --git a/challenges/default-generic-arguments/tsconfig.json b/challenges/default-generic-arguments/tsconfig.json new file mode 100644 index 0000000..d8de5a2 --- /dev/null +++ b/challenges/default-generic-arguments/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true + } +} diff --git a/challenges/default-generic-arguments/user.ts b/challenges/default-generic-arguments/user.ts new file mode 100644 index 0000000..cd95410 --- /dev/null +++ b/challenges/default-generic-arguments/user.ts @@ -0,0 +1,8 @@ +type ApiRequest = { + data: T; + method: TMethod; +}; + +type TSConfig = { + [K in keyof T]: T[K]; +}; diff --git a/challenges/generic-function-arguments/metadata.json b/challenges/generic-function-arguments/metadata.json new file mode 100644 index 0000000..4bd7c39 --- /dev/null +++ b/challenges/generic-function-arguments/metadata.json @@ -0,0 +1,8 @@ +{ + "id": "generic-function-arguments", + "label": "Generic Function Arguments", + "description": "Give generic function types to your functions", + "difficulty": "beginner", + "prerequisites": ["generic-type-arguments", "literal-types"], + "author": "dimitropoulos" +} diff --git a/challenges/generic-function-arguments/prompt.md b/challenges/generic-function-arguments/prompt.md new file mode 100644 index 0000000..43c69fd --- /dev/null +++ b/challenges/generic-function-arguments/prompt.md @@ -0,0 +1,76 @@ +## Why Functions Need Generics + +One of the most common needs a TypeScript developer faces is the need to pass generic type arguments to functions. Then, those functions can use and manipulate those parameters. + +In many situations, we don't need functions to be generic. The function might be written in such a way as to only work for one type of data (e.g. `string` or `number`). But, other times, we write functions where we want to pass the data type along from input to output. + +## Understanding Generic Syntax + +In a previous challenge on [generic type arguments](https://typehero.dev/challenge/generic-type-arguments) we saw a `Row` type that looked like this: + +```ts +interface Row { + label: string; + value: T; + disabled: boolean; +} +``` + +Imagine if you had a function `createRow` that returns an object that looked like our `Row` type: + +```ts +const createRow = ( + label: string, + + // how do we provide a type for `value`? + value: unknown, + + disabled = false, +) => ({ + label, + value, + disabled, +}); +``` + +Similarly to how we used the `<` and `>` for our `Row` interface, we place the `<` and `>` right before the parenthesis that start our function's arguments. + +```ts +const createRow = (label: string, value: T, disabled = false) => ({ + //... +}); +``` + +This is great! Now our `value` parameter has the right type! + +### A Word On A Syntax Quirk + +You may see the `` syntax out in the wild. There's an unfortunate inconvenience with TypeScript's syntax that this is used to work around. Imagine being a parser and seeing `const createRow = `. If you don't know what's coming next it could be ambiguous between these two things + +```ts +// Thing 1: Start of JSX +const createRow = Some JSX stuff!; + +// Thing 2: start of generic function +const createRow = (someArg: T) => { + /* some function stuff */ +} +``` + +Because of this ambiguity, we need to do something to _disambiguate_ these two situations to the TypeScript compiler. In the early days it was common to use `` (or `` before `unknown` existed in TypeScript); however, over time people realized there was an easier way: ``. Since `,` is invalid in JSX, but valid in TypeScript when separating multiple arguments, it's appropriate to use this to disambiguate the two situations to the TypeScript compiler. + +With that being said, if you see this strange syntax, please note that it is not you, it's just TypeScript being a bit funky. If this is confusing to you, just skip it and come back here and read it again when the day comes that you first see this syntax out in the wild. + +## Solving This Challenge + +This challenge asks you to make an `identity` function. This function does nothing and returns what it was passed with no modifications. + +> Note: mathematicians love `identity` operations, so computer scientists use them too so we can seem cool to the mathematicians. + +The next function (`mapArray`) is admittedly pretty hard, but take it slow and think about it one step at a time. Ask yourself: + +> what is the type of `arr` + +Then, provide a generic argument as a type for `arr`. After you do that, think about what types you need to provide for `fn`. + +In this particular case, if you spend a few minutes with it don't be afraid to check out some solutions. The second example is here to widen your perspective more than anything else. It's good to wrestle with tough problems, even ones that are above your ability level. diff --git a/challenges/generic-function-arguments/solutions/1.ts b/challenges/generic-function-arguments/solutions/1.ts new file mode 100644 index 0000000..047474a --- /dev/null +++ b/challenges/generic-function-arguments/solutions/1.ts @@ -0,0 +1,3 @@ +const identity = (input: T) => input; + +const mapArray = (arr: T[], fn: (item: T) => U) => arr.map(fn); diff --git a/challenges/generic-function-arguments/tests.ts b/challenges/generic-function-arguments/tests.ts new file mode 100644 index 0000000..0055d54 --- /dev/null +++ b/challenges/generic-function-arguments/tests.ts @@ -0,0 +1,58 @@ +import { Expect, Equal } from 'type-testing'; + +/** temporary */ +const expect = (a: T) => ({ + toEqual: (b: T) => a === b +}); + +const identity_string = identity("this is a string"); +expect(identity_string).toEqual("this is a string"); +type test_identity_string = Expect>; + +const identity_number = identity(123.45); +expect(identity_number).toEqual(123.45); +type test_identity_number = Expect>; + +const identity_boolean = identity(false); +expect(identity_boolean).toEqual(false); +type test_identity_boolean = Expect>; + +const strings = ['1', '1', '2', '3', '5']; +const numbers = [1, 1, 2, 3, 5]; + +const stringsToNumbers = mapArray(strings, str => parseInt(str)); +expect(stringsToNumbers).toEqual(numbers); +type test_stringsToNumber = Expect>; + +const numbersToStrings = mapArray(numbers, num => `${num}`); +expect(numbersToStrings).toEqual(strings) +type test_numbersToStrings = Expect>; + +const numbersToNumbers = mapArray(numbers, num => num + 1); +expect(numbersToNumbers).toEqual([2, 2, 3, 4, 6]) +type test_numbersToNumbers = Expect>; + +const stringsToStrings = mapArray(strings, str => `${str}!`); +expect(stringsToStrings).toEqual(['1!', '1!', '2!', '3!', '5!']) +type test_stringsToStrings = Expect>; diff --git a/challenges/generic-function-arguments/tsconfig.json b/challenges/generic-function-arguments/tsconfig.json new file mode 100644 index 0000000..d8de5a2 --- /dev/null +++ b/challenges/generic-function-arguments/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true + } +} diff --git a/challenges/generic-function-arguments/user.ts b/challenges/generic-function-arguments/user.ts new file mode 100644 index 0000000..fa6cb9f --- /dev/null +++ b/challenges/generic-function-arguments/user.ts @@ -0,0 +1,3 @@ +const identity = (val: T) => val; + +const mapArray = (arr: T[], fn: (val: T) => U) => arr.map(fn); diff --git a/challenges/indexed-types/metadata.json b/challenges/indexed-types/metadata.json new file mode 100644 index 0000000..eb7411f --- /dev/null +++ b/challenges/indexed-types/metadata.json @@ -0,0 +1,8 @@ +{ + "id": "indexed-types", + "label": "Indexed Types", + "description": "Some types can be indexed to lookup a particular value.", + "difficulty": "beginner", + "prerequisites": ["primitive-data-types", "type-unions", "keyof", "literal-types"], + "author": "dimitropoulos" +} diff --git a/challenges/indexed-types/prompt.md b/challenges/indexed-types/prompt.md new file mode 100644 index 0000000..19ca58c --- /dev/null +++ b/challenges/indexed-types/prompt.md @@ -0,0 +1,97 @@ +## What Indexed Types Are For + +Indexed types allow you to look inside other types. They're a useful tool when accessing array/tuple values, object values, and more! We're going to see much more complicated uses for indexed types, but for this challenge, let's just focus on the basics. + +## How To Use Index Types + +### Arrays + +Just how you can use a number index to grab an element of an array: + +```ts +const cars = ['Bugatti', 'Ferarri', 'Lambo', 'Porsche', 'Toyota Corolla']; +const secondCar = cars[1]; +``` + +You can do exactly the same thing with types; + +```ts +type Cars = ['Bugatti', 'Ferarri', 'Lambo', 'Porsche', 'Toyota Corolla']; +type SecondCar = Cars[1]; +// ^? +``` + +> Notice that TypeScript evaluates the literal value `"Ferarri"`. It's not just `string`! + +### Objects + +Same for objects: + +```ts +const donations = { + Bono: 15_000_000, + 'J.K. Rowling': 160_000_000, + 'Taylor Swift': 45_000_000, + 'Elton John': 600_000_000, + 'Angelina Jolie and Brad Pitt': 100_000_000, +}; +const eltonDonations = donations['Elton John']; +``` + +and (you guessed it!) object types: + +```ts +type Donations = { + Bono: 15_000_000; + 'J.K. Rowling': 160_000_000; + 'Taylor Swift': 45_000_000; + 'Elton John': 600_000_000; + 'Angelina Jolie and Brad Pitt': 100_000_000; +}; + +type EltonDonations = Donations['Elton John']; +// ^? +``` + +> Notice that TypeScript evaluates the literal value for Elton's donations. It's not just `number`! + +### Strings + +If you're getting excited that you might be able to do the same for strings, I have somewhat bad news for you, unfortunately. + +The following is valid TypeScript + +```ts +const question = 'Have the humans delivered their ultimate cup of coffee?'; +const firstCharacter = question[0]; +``` + +And this is also valid: + +```ts +type Question = 'Have the humans delivered their ultimate cup of coffee?'; +type FirstCharacter = Question[0]; +// ^? +``` + +But perhaps you notice a difference from the above examples that returned a literal type. Here, with TypeScript strings, the resultant value is just `string` and not a more specific constant value like `H` (the first character of the string type). That means that while it's not a syntax error to index a string type, it also effectively _does nothing_ since it will always result in `string`. + +In fact, TypeScript doesn't even check (or care about) the specific value you index with. You'll always get back string: + +```ts +type Empty = ''; +type IndexEmpty = Empty[9001]; +// ^? +``` + +Not a big deal or anything, but just something to be aware of in case you were expecting a different behavior. + +> _spoiler alert_: there actually _ARE_ ways to index a string and return a particular character, but ends up involving some quite advanced type machinery. + +## Solving This Challenge + +This one should feel easy to pass. Let the tests guide you. + +And, remember, while this challenge is pretty straightforward to complete, we're soon going to be doing some pretty advanced work that relies heavily on indexed types. + +Onward! diff --git a/challenges/indexed-types/solutions/1.ts b/challenges/indexed-types/solutions/1.ts new file mode 100644 index 0000000..88ca8df --- /dev/null +++ b/challenges/indexed-types/solutions/1.ts @@ -0,0 +1,2 @@ +type TheCoolestCarEverMade = Cars[4]; +type TruckDriverBonusGiver = Donations['Taylor Swift']; diff --git a/challenges/indexed-types/tests.ts b/challenges/indexed-types/tests.ts new file mode 100644 index 0000000..56ec81e --- /dev/null +++ b/challenges/indexed-types/tests.ts @@ -0,0 +1,22 @@ +import { Expect, Equal } from 'type-testing'; + +type Cars = ["Bugatti", "Ferarri", "Lambo", "Porsche", "Toyota Corolla"] + +type Donations = { + "Bono": 15_000_000, + "J.K. Rowling": 160_000_000, + "Taylor Swift": 45_000_000, + "Elton John": 600_000_000, + "Angelina Jolie and Brad Pitt": 100_000_000, +}; + +type test_TheCoolestCarEverMade = Expect>; + +type test_TruckDriverBonusGiver = Expect>; + diff --git a/challenges/indexed-types/tsconfig.json b/challenges/indexed-types/tsconfig.json new file mode 100644 index 0000000..d8de5a2 --- /dev/null +++ b/challenges/indexed-types/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true + } +} diff --git a/challenges/indexed-types/user.ts b/challenges/indexed-types/user.ts new file mode 100644 index 0000000..b2cf65e --- /dev/null +++ b/challenges/indexed-types/user.ts @@ -0,0 +1,2 @@ +type TheCoolestCarEverMade = Cars[4]; +type TruckDriverBonusGiver = Donations["Taylor Swift"]; diff --git a/challenges/mapped-object-types/metadata.json b/challenges/mapped-object-types/metadata.json new file mode 100644 index 0000000..f41e018 --- /dev/null +++ b/challenges/mapped-object-types/metadata.json @@ -0,0 +1,8 @@ +{ + "id": "mapped-object-types", + "label": "Mapped Object Types", + "description": "Mapping objects is a foundational skill that allows you to write type translation logic.", + "difficulty": "beginner", + "prerequisites": ["index-signatures", "keyof", "generic-type-arguments", "indexed-types"], + "author": "dimitropoulos" +} diff --git a/challenges/mapped-object-types/prompt.md b/challenges/mapped-object-types/prompt.md new file mode 100644 index 0000000..16cda74 --- /dev/null +++ b/challenges/mapped-object-types/prompt.md @@ -0,0 +1,79 @@ +## Why Mapped Object Types Are Useful + +In mathematics, a "map" is a function that associates each element in one set with an element. That's where we get this usage of the word in computer science. + +Mapped types are one of the most potent and versatile features of TypeScript. They enable you to create new types by transforming the properties of existing types, making your code more robust, flexible, and maintainable. As you further your journey with TypeScript, you're going to find yourself reaching for mapped types pretty often to solve daily-living type (and data) transformation tasks. + +## How To Use Mapped Types + +It might seem a little strange, but let's start with a mapped type that _does nothing_. Here's how it looks: + +```ts +type MoviesByGenre = { + action: 'Die Hard'; + comedy: 'Groundhog Day'; + sciFi: 'Blade Runner'; + fantasy: 'The Lord of the Rings: The Fellowship of the Ring'; + drama: 'The Shawshank Redemption'; + horror: 'The Shining'; + romance: 'Titanic'; + animation: 'Toy Story'; + thriller: 'The Silence of the Lambs'; +}; + +type MovieInfoByGenre = { + [K in keyof T]: T[K]; +}; + +type Example = MovieInfoByGenre; +``` + +Let's talk about what this syntax does, piece by piece: + +- `type` begins a type declaration. +- `MovieInfoByGenre` is the name we gave to our type +- `` defines generic parameter we named `T` +- `= {` begins our type +- `[` starts an index signature +- `K` is the name we're giving to the value in this index signature. + - `K` (for "Key") and `P` (for "Property") are very common names to use in this particular situation. While it's not often the best choice, you'll see it around (including in TypeScript's own builtin types), so it's worthwhile to just roll with it if you see it. If you can make a more descriptive name, you should. +- `in` is a special TypeScript operator for mapping. It tells TypeScript that `K` represents a single item in a larger set of things defined on the right side of the `in` operator. +- `keyof T` is a union of all the keys of the `T` object. +- `: T[K]` is regular type indexing. The value, `K`, can here be thought of as a singular property in a sort of loop. We take some individual value of `K` (which are keys of `T`) and then index `T` with that key, thereby producing a value that matches the value in `T` for that `K`. +- `;` it might at first seem strange, but if you take a step back, this is just a `property: value` line in an object type, so we end it with a semicolon like we normally would with types in TypeScript. +- `};` ends the type declaration. + +But, all this _stuff_ but we end up with a type that's literally identical to what we started with. In the above code, `Example` and `MoviesByGenre` are literally the same type to TypeScript because TypeScript is a structural type system. They have all the same properties and values, so to TypeScript they are basically aliases at this point (aliases with extra steps, granted!). + +### Resetting all values + +So let's do the simplest possible thing to change our `Example` type. Let's just change one little part of the above: + +```diff +type MovieInfoByGenre = { +- [K in keyof T]: T[K]; ++ [K in keyof T]: boolean; +}; +``` + +Can you picture what our `Example` type would be if we made this change? + +It would look like this: + +```ts +type Example = { + action: boolean; + comedy: boolean; + sciFi: boolean; + fantasy: boolean; + drama: boolean; + horror: boolean; + romance: boolean; + animation: boolean; + thriller: boolean; +}; +``` + +## Solving This Challenge + +The tests define a slightly more complicated remapping, but you actually have all the tools you need to make it work. Just take it slow and follow the syntax piece by piece if you get lost. diff --git a/challenges/mapped-object-types/solutions/1.ts b/challenges/mapped-object-types/solutions/1.ts new file mode 100644 index 0000000..e172ed5 --- /dev/null +++ b/challenges/mapped-object-types/solutions/1.ts @@ -0,0 +1,7 @@ +type MovieInfoByGenre = { + [K in keyof T]: { + name: string; + year: number; + director: string; + }; +}; diff --git a/challenges/mapped-object-types/tests.ts b/challenges/mapped-object-types/tests.ts new file mode 100644 index 0000000..a09f547 --- /dev/null +++ b/challenges/mapped-object-types/tests.ts @@ -0,0 +1,112 @@ +import { Expect, Equal } from 'type-testing'; + +type MoviesByGenre = { + action: 'Die Hard'; + comedy: 'Groundhog Day'; + sciFi: 'Blade Runner'; + fantasy: 'The Lord of the Rings: The Fellowship of the Ring'; + drama: 'The Shawshank Redemption'; + horror: 'The Shining'; + romance: 'Titanic'; + animation: 'Toy Story'; + thriller: 'The Silence of the Lambs'; +}; + +const test_MoviesInfoByGenre: MovieInfoByGenre = { + action: { + name: 'Die Hard', + year: 1988, + director: 'John McTiernan', + }, + comedy: { + name: 'Groundhog Day', + year: 1993, + director: 'Harold Ramis', + }, + sciFi: { + name: 'Blade Runner', + year: 1982, + director: 'Ridley Scott', + }, + fantasy: { + name: 'The Lord of the Rings: The Fellowship of the Ring', + year: 2001, + director: 'Peter Jackson', + }, + drama: { + name: 'The Shawshank Redemption', + year: 1994, + director: 'Frank Darabont', + }, + horror: { + name: 'The Shining', + year: 1980, + director: 'Stanley Kubrick', + }, + romance: { + name: 'Titanic', + year: 1997, + director: 'James Cameron', + }, + animation: { + name: 'Toy Story', + year: 1995, + director: 'John Lasseter', + }, + thriller: { + name: 'The Silence of the Lambs', + year: 1991, + director: 'Jonathan Demme', + }, +}; + +type test_MovieInfoByGenre = Expect, + { + action: { + name: string; + year: number; + director: string; + }; + comedy: { + name: string; + year: number; + director: string; + }; + sciFi: { + name: string; + year: number; + director: string; + }; + fantasy: { + name: string; + year: number; + director: string; + }; + drama: { + name: string; + year: number; + director: string; + }; + horror: { + name: string; + year: number; + director: string; + }; + romance: { + name: string; + year: number; + director: string; + }; + animation: { + name: string; + year: number; + director: string; + }; + thriller: { + name: string; + year: number; + director: string; + }; + } +>>; diff --git a/challenges/mapped-object-types/tsconfig.json b/challenges/mapped-object-types/tsconfig.json new file mode 100644 index 0000000..d8de5a2 --- /dev/null +++ b/challenges/mapped-object-types/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true + } +} diff --git a/challenges/mapped-object-types/user.ts b/challenges/mapped-object-types/user.ts new file mode 100644 index 0000000..ce51a36 --- /dev/null +++ b/challenges/mapped-object-types/user.ts @@ -0,0 +1,7 @@ +type MovieInfoByGenre = { + [K in keyof T]: { + name: string; + year: number; + director: string; + }; +}; diff --git a/challenges/primitive-data-types/user.ts b/challenges/primitive-data-types/user.ts index bd8a048..79fc69c 100644 --- a/challenges/primitive-data-types/user.ts +++ b/challenges/primitive-data-types/user.ts @@ -1,5 +1,5 @@ const playSong = (artistName: string, year: number): string => { - return `${artistName} was released in the year ${year}`; + return `${artistName} was released in the year ${year}`; }; const artistName: string = "Frank Zappa"; @@ -7,17 +7,17 @@ const artistName: string = "Frank Zappa"; const age: number = 52; interface Musician { - artistName: string; - age: number; - deceased: boolean; + artistName: string; + age: number; + deceased: boolean; } const musicianInfo = ({ artistName, age, deceased }: Musician) => { - return `${artistName}, age ${age}${deceased ? " (deceased)" : ""}`; + return `${artistName}, age ${age}${deceased ? " (deceased)" : ""}`; }; musicianInfo({ - artistName, - age, - deceased: true, + artistName, + age, + deceased: true, }); diff --git a/challenges/type-aliases/user.ts b/challenges/type-aliases/user.ts index f8ff0f6..87922cf 100644 --- a/challenges/type-aliases/user.ts +++ b/challenges/type-aliases/user.ts @@ -9,9 +9,9 @@ type IsOperational = boolean; type Kilograms = number; type Count = number; type Payload = { - name: Name; - mass: Kilograms; - // the tests show that you need a `mass` property here - // but first you might need to create an alias for `Kilograms` - // because that's the value of `mass` + name: Name; + mass: Kilograms; + // the tests show that you need a `mass` property here + // but first you might need to create an alias for `Kilograms` + // because that's the value of `mass` }; diff --git a/challenges/typeof/metadata.json b/challenges/typeof/metadata.json new file mode 100644 index 0000000..08f38ab --- /dev/null +++ b/challenges/typeof/metadata.json @@ -0,0 +1,8 @@ +{ + "id": "typeof", + "label": "The `typeof` Operator", + "description": "When you need to create a type from existing JavaScript values, `typeof` is the tool for the job.", + "difficulty": "beginner", + "prerequisites": ["literal-types", "type-unions", "type-aliases"], + "author": "dimitropoulos" +} diff --git a/challenges/typeof/prompt.md b/challenges/typeof/prompt.md new file mode 100644 index 0000000..40922e9 --- /dev/null +++ b/challenges/typeof/prompt.md @@ -0,0 +1,91 @@ +## `typeof`: A Bridge Between Worlds + +Have we said thank you, yet, for taking these TypeHero challenges? Well... THANK YOU! + +You're doing yourself a great service by improving your knowledge of TypeScript. As you dig deeper, on your quest to become a TypeScript wizard, you're going to start noticing that there are almost two worlds. The "JavaScript" world and the "types" world. + +| | JavaScript world | types world | +| ----------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| variables | `const`, `let`, function arguments | [type aliases](https://typehero.dev/challenge/type-aliases), [default generic arguments](https://typehero.dev/challenge/default-generic-arguments) | +| operations | `for` loops, `while` loops, recursion, higher-order functions | recursion, higher-order types | +| runtime artifacts | ✅ | ❌ | +| time of error | runtime | compile time | +| constructs | statements, expressions | expressions | + +Perhaps you see a problem this divide might cause. What happens if you have information in JavaScript that you want to bring over to TypeScript? + + + +```text +raw data +| conversion +| | types +| | | +v v v +JavaScript ==> `typeof` ==> TypeScript +``` + +## How to use `typeof` + +### `typeof` in JavaScript + +You might be familiar with `typeof` as a JavaScript operator. It returns a string indicating the type of a JavaScript value. + +```ts +console.log(typeof "Euler's Number"); +// -> logs the string `'string'` + +console.log(typeof 2.7182); +// -> logs the string `'number'` +``` + +But that's JavaScript. The `typeof` actually also exists in TypeScript! + +### `typeof` in TypeScript + +What if we wanted to use some of our data as a type in TypeScript? + +```ts +const name = "Euler's Number"; +const value = 2.7182; + +// we can use `typeof` for type aliases +type Value = typeof value; + +interface FamousNumbers { + // or we can use it inline + label: typeof name; + value: Value; +} +``` + +```ts +const count = 42; +type Count = typeof count; +``` + +### `typeof` Is Most Useful For Complex Types + +`typeof` is a TypeScript keyword that you put before a JavaScript _identifier_ like a variable name. Let's say you have this function in your codebase: + +```ts +const createPoint = (x: number, y: number) => ({ x, y }); +``` + +You can use `typeof` to _extract_ the type of this JavaScript function and bring it into the realm of TypeScript types: + +```ts +// JavaScript stuff +// | +// v---------- +type CreatePoint = typeof createPoint; +//^---------------------- +//| +//|TypeScript stuff +``` + +Later, you're going to learn about ways to then do more operations on types. We're going to be extracting keys of objects , and creating new types for return types and parameters of functions. + +## Solving This Challenge + +The tests provide you some data. Try to make all the tests pass by using that data combined with the `typeof` operator. diff --git a/challenges/typeof/solutions/1.ts b/challenges/typeof/solutions/1.ts new file mode 100644 index 0000000..21aac10 --- /dev/null +++ b/challenges/typeof/solutions/1.ts @@ -0,0 +1,6 @@ +type Width = typeof width; +type Margin = typeof margin; +type Data = typeof d3ChartConfig.data; +type YScale = typeof d3ChartConfig.xScale; + +type D3ChartConfig = typeof d3ChartConfig; diff --git a/challenges/typeof/tests.ts b/challenges/typeof/tests.ts new file mode 100644 index 0000000..9de5920 --- /dev/null +++ b/challenges/typeof/tests.ts @@ -0,0 +1,100 @@ +import { Expect, Equal } from 'type-testing'; + +const height = 500; +const width = 700; + +type test_Width = Expect>; + +const margin = { + top: 20, + right: 30, + bottom: 40, + left: 50, +}; + +type test_Margin = Expect>; + +const d3ChartConfig = { + width, + height, + margin, + data: [ + { category: 'A', value: 30 }, + { category: 'B', value: 45 }, + { category: 'C', value: 60 }, + { category: 'D', value: 25 }, + { category: 'E', value: 50 }, + ], + xScale: { + type: 'band', + domain: [0, 100], + range: [0, width - margin.right - margin.left], + }, + yScale: { + type: 'linear', + domain: [0, 100], + range: [height - margin.bottom, margin.top], + }, + xAxis: { + label: 'Categories', + tickSize: 5, + }, + yAxis: { + label: 'Values', + tickSize: 5, + }, + bar: { + fill: 'rebeccapurple', + }, +}; + +type test_Data = Expect>; + +type test_YScale = Expect>; + +type test_d3ChartConfig = Expect>; diff --git a/challenges/typeof/tsconfig.json b/challenges/typeof/tsconfig.json new file mode 100644 index 0000000..d8de5a2 --- /dev/null +++ b/challenges/typeof/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true + } +} diff --git a/challenges/typeof/user.ts b/challenges/typeof/user.ts new file mode 100644 index 0000000..21aac10 --- /dev/null +++ b/challenges/typeof/user.ts @@ -0,0 +1,6 @@ +type Width = typeof width; +type Margin = typeof margin; +type Data = typeof d3ChartConfig.data; +type YScale = typeof d3ChartConfig.xScale; + +type D3ChartConfig = typeof d3ChartConfig;