feat(curriculum): Add type safe user profile to typescript module (#66005)

Co-authored-by: Sem Bauke <sem@freecodecamp.org>
Co-authored-by: Dario <105294544+Dario-DC@users.noreply.github.com>
Co-authored-by: Kolade <chrisjay967@gmail.com>
This commit is contained in:
Jessica Wilkins
2026-04-07 02:50:32 -07:00
committed by GitHub
parent 0e8da837be
commit 2ad6e448fa
16 changed files with 877 additions and 0 deletions
+6
View File
@@ -6181,6 +6181,12 @@
"In these lessons, you will learn what TypeScript is and how to use it."
]
},
"workshop-type-safe-user-profile": {
"title": "Build a Type Safe User Profile",
"intro": [
"In this workshop, you will practice working with type annotations, array types, object types and more by building out a user profile."
]
},
"lecture-understanding-type-composition": {
"title": "Understanding Type Composition",
"intro": [
@@ -0,0 +1,78 @@
---
id: 699b4e284291adcfcb90df47
title: Step 1
challengeType: 1
dashedName: step-1
---
# --description--
In this workshop, you will practice working with primitive types as well as object and array types by building out a type safe user profile.
Start by creating a variable called `profile` and assign it an object. Your object should have these three properties and values:
```md
username: "codeLearner",
age: 25,
isLoggedIn: false,
```
# --hints--
You should have a variable called `profile`.
```js
assert.exists(profile);
```
Your `profile` variable should be an object.
```js
assert.isObject(profile);
```
You should have a `username` property in your `profile` object.
```js
assert.property(profile, "username");
```
Your `username` property should have a value of `"codeLearner"`.
```js
assert.propertyVal(profile, "username", "codeLearner");
```
Your `profile` object should have an `age` property.
```js
assert.property(profile, "age");
```
Your `age` property should have a value of `25`.
```js
assert.propertyVal(profile, "age", 25);
```
Your `profile` object should have an `isLoggedIn` property.
```js
assert.property(profile, "isLoggedIn");
```
Your `isLoggedIn` property should have a value of `false`.
```js
assert.propertyVal(profile, "isLoggedIn", false);
```
# --seed--
## --seed-contents--
```ts
--fcc-editable-region--
--fcc-editable-region--
```
@@ -0,0 +1,43 @@
---
id: 699b5370a43e299b4673bcaa
title: Step 2
challengeType: 1
dashedName: step-2
---
# --description--
It would be helpful to see the `profile` object properties in the console as you build it out.
Add a `console.log` that logs `profile` to the console.
# --before-each--
```js
const spy = __helpers.spyOn(console, 'log');
const getLogs = () => spy.calls.map(call => call?.[0]);
```
# --hints--
You should log `profile` to the console.
```js
assert.equal(getLogs()[0], profile);
```
# --seed--
## --seed-contents--
```ts
const profile = {
username: "codeLearner",
age: 25,
isLoggedIn: false,
}
--fcc-editable-region--
--fcc-editable-region--
```
@@ -0,0 +1,43 @@
---
id: 699b58c5abecfda9dc81c4ec
title: Step 3
challengeType: 1
dashedName: step-3
---
# --description--
Right now the `profile` object only has three properties. But it would be nice to have a few more.
Add a property called `mood` to the `profile` object. Its value should be `null`.
# --hints--
Your `profile` object should have a `mood` property.
```js
assert.property(profile, "mood");
```
Your `mood` property should have a value of `null`.
```js
assert.isNull(profile?.mood);
```
# --seed--
## --seed-contents--
```ts
--fcc-editable-region--
const profile = {
username: "codeLearner",
age: 25,
isLoggedIn: false,
}
--fcc-editable-region--
console.log(profile);
```
@@ -0,0 +1,74 @@
---
id: 699b5b7cb68b6d35afca61b7
title: Step 4
challengeType: 1
dashedName: step-4
---
# --description--
While it is possible to continue to add properties like this to the `profile` object, it would be nice to restrict the types of properties that should be allowed. It would be better to define the shape of the `profile` object beforehand.
In prior lessons, you learned how to work with object types like this:
```ts
const composer: {
name: string;
instrument: string;
isActive: boolean;
} = {
name: "Ludwig van Beethoven",
instrument: "Piano",
isActive: false
};
```
In this example, `composer` is an object with an inline object type. This object has three required properties of `name`, `instrument` and `isActive`. The first two properties must be of type `string` and `isActive` must be of type `boolean`.
Update your existing `profile` object to include an inline object type. `username` should be typed to a `string`, `age` should be typed to a `number` and `isLoggedIn` should be typed to a `boolean`.
# --hints--
Your `profile` object should have an inline object type with a `username` property set to `string`.
```js
const explorer = await __helpers.Explorer(code);
const { profile } = explorer.variables;
const prop = { name: "username", type: "string" };
assert.isTrue(profile.annotation.hasTypeProps(prop));
```
Your `profile` object should have an inline object type with an `age` property set to `number`.
```js
const explorer = await __helpers.Explorer(code);
const { profile } = explorer.variables;
const prop = { name: "age", type: "number" };
assert.isTrue(profile.annotation.hasTypeProps(prop));
```
Your `profile` object should have an inline object type with an `isLoggedIn` property set to `boolean`.
```js
const explorer = await __helpers.Explorer(code);
const { profile } = explorer.variables;
const prop = { name: "isLoggedIn", type: "boolean" };
assert.isTrue(profile.annotation.hasTypeProps(prop));
```
# --seed--
## --seed-contents--
```ts
--fcc-editable-region--
const profile = {
username: "codeLearner",
age: 25,
isLoggedIn: false,
mood: null
}
--fcc-editable-region--
console.log(profile);
```
@@ -0,0 +1,47 @@
---
id: 699b61ca8c9cb9bc1968364e
title: Step 5
challengeType: 1
dashedName: step-5
---
# --description--
If you open up the console, you should see the following error message:
```md
Object literal may only specify known properties, and 'mood' does not exist in type '{ username: string; age: number; isLoggedIn: boolean; }'.
```
In vanilla JavaScript, you were allowed to add whatever properties and values you liked to the `profile` object. But in TypeScript, the compiler checks that the object only has the properties you defined (`username`, `age`, and `isLoggedIn` in this case). Since `mood` wasn't included in the type, TypeScript treats it as an invalid property and throws an error.
Remove the `mood: null` from your code.
# --hints--
You should no longer have `mood: null` in your code.
```js
assert.isUndefined(profile.mood);
```
# --seed--
## --seed-contents--
```ts
const profile: {
username: string;
age: number;
isLoggedIn: boolean;
} = {
username: "codeLearner",
age: 25,
isLoggedIn: false
--fcc-editable-region--
mood: null
--fcc-editable-region--
};
console.log(profile);
```
@@ -0,0 +1,44 @@
---
id: 699b62c99ec8c63470313365
title: Step 6
challengeType: 1
dashedName: step-6
---
# --description--
Now it is time to add another property to your `profile` object.
Start by updating the inline object type to include a `bio` property with a type set to `string`.
# --hints--
You should update the inline object type to include a `bio` property set to `string`.
```js
const explorer = await __helpers.Explorer(code);
const { profile } = explorer.variables;
const prop = { name: "bio", type: "string" };
assert.isTrue(profile.annotation.hasTypeProps(prop));
```
# --seed--
## --seed-contents--
```ts
const profile: {
--fcc-editable-region--
username: string;
age: number;
isLoggedIn: boolean;
--fcc-editable-region--
} = {
username: "codeLearner",
age: 25,
isLoggedIn: false
};
console.log(profile);
```
@@ -0,0 +1,65 @@
---
id: 699b64096301fd75e3efeb1b
title: Step 7
challengeType: 1
dashedName: step-7
---
# --description--
If you open the console again, you will see a different error message:
```md
Property 'bio' is missing in type '{ username: string; age: number; isLoggedIn: false; }' but required in type '{ username: string; age: number; isLoggedIn: boolean; bio: string; }'.
```
As you recall in prior lessons, you can mark properties as optional by using the `?` symbol next to the property name like this:
```ts
const musician: {
name: string;
instrument: string;
isActive: boolean;
numberOfAlbums?: number;
} = {
name: "Ludwig van Beethoven",
instrument: "Piano",
isActive: false
};
```
In this example, `numberOfAlbums` is marked optional so TypeScript will not throw an error in this case.
Update your `bio` property to be marked as optional.
# --hints--
Your `bio` property should be marked as optional.
```js
const explorer = await __helpers.Explorer(code);
const { profile } = explorer.variables;
const prop = { name: "bio", type: "string", isOptional: true };
assert.isTrue(profile.annotation.hasTypeProps(prop));
```
# --seed--
## --seed-contents--
```ts
const profile: {
username: string;
age: number;
isLoggedIn: boolean;
--fcc-editable-region--
bio: string
--fcc-editable-region--
} = {
username: "codeLearner",
age: 25,
isLoggedIn: false
};
console.log(profile);
```
@@ -0,0 +1,64 @@
---
id: 699b65caf17a8e8d340af27d
title: Step 8
challengeType: 1
dashedName: step-8
---
# --description--
Now it is time to add the last property to the `profile` object.
In prior lessons, you learned how to work with array types. You learned about different array types like `string[]`, or `number[]`:
```ts
const scores: number[] = [95, 87, 100];
```
In this example, the `scores` array only accepts numbers. If you try to add another type like a `string`, or `null` then TypeScript will throw an error.
Update your `profile` object to add a `programmingLanguages` property, typed as a `string[]`. Inside the object literal, set this property to `["JavaScript", "Python", "C++"]`.
# --hints--
You should have a `programmingLanguages` property inside of your `profile` object.
```js
assert.property(profile, "programmingLanguages");
```
Your `programmingLanguages` property should have the value of `["JavaScript", "Python", "C++"]`.
```js
assert.deepEqual(profile.programmingLanguages, ["JavaScript", "Python", "C++"]);
```
Your `programmingLanguages` property should be typed as a `string[]`.
```js
const explorer = await __helpers.Explorer(code);
const { profile } = explorer.variables;
const prop = { name: "programmingLanguages", type: "string[]" };
assert.isTrue(profile.annotation.hasTypeProps(prop));
```
# --seed--
## --seed-contents--
```ts
--fcc-editable-region--
const profile: {
username: string;
age: number;
isLoggedIn: boolean;
bio?: string;
} = {
username: "codeLearner",
age: 25,
isLoggedIn: false
};
--fcc-editable-region--
console.log(profile);
```
@@ -0,0 +1,93 @@
---
id: 699b68f94a0d154c2a7a5ce0
title: Step 9
challengeType: 1
dashedName: step-9
---
# --description--
Now that the `profile` object is complete, it is time to build out a new object which represents the user roles.
Create a new variable called `userRoles` and assign it an object. Your object should have the following properties and values:
```md
admin: "full-access",
editor: "limited-access",
viewer: "read-only"
```
# --hints--
You should have a variable called `userRoles`.
```js
assert.isDefined(userRoles);
```
Your `userRoles` variable should be an object.
```js
assert.isObject(userRoles);
```
Your `userRoles` object should have an `admin` property.
```js
assert.property(userRoles, "admin");
```
Your `admin` property should have a value of `"full-access"`.
```js
assert.propertyVal(userRoles, "admin", "full-access");
```
Your `userRoles` object should have an `editor` property.
```js
assert.property(userRoles, "editor");
```
Your `editor` property should have a value of `"limited-access"`.
```js
assert.propertyVal(userRoles, "editor", "limited-access");
```
Your `userRoles` object should have a `viewer` property.
```js
assert.property(userRoles, "viewer");
```
Your `viewer` property should have a value of `"read-only"`.
```js
assert.propertyVal(userRoles, "viewer", "read-only");
```
# --seed--
## --seed-contents--
```ts
const profile: {
username: string;
age: number;
isLoggedIn: boolean;
bio?: string;
programmingLanguages: string[];
} = {
username: "codeLearner",
age: 25,
isLoggedIn: false,
programmingLanguages: ["JavaScript", "Python", "C++"]
};
console.log(profile);
--fcc-editable-region--
--fcc-editable-region--
```
@@ -0,0 +1,58 @@
---
id: 699b6d91cde15459d3619c4a
title: Step 10
challengeType: 1
dashedName: step-10
---
# --description--
Just like the `profile` object, it would be nice to see the `userRoles` object in the console.
Add a `console.log()` to log the `userRoles` object.
# --before-each--
```js
const spy = __helpers.spyOn(console, 'log');
const getLogs = () => spy.calls.map(call => call?.[0]);
```
# --hints--
You should log `userRoles` to the console.
```js
assert.equal(getLogs()[1], userRoles);
```
# --seed--
## --seed-contents--
```ts
const profile: {
username: string;
age: number;
isLoggedIn: boolean;
bio?: string;
programmingLanguages: string[];
} = {
username: "codeLearner",
age: 25,
isLoggedIn: false,
programmingLanguages: ["JavaScript", "Python", "C++"]
};
console.log(profile);
const userRoles = {
admin: "full-access",
editor: "limited-access",
viewer: "read-only"
};
--fcc-editable-region--
--fcc-editable-region--
```
@@ -0,0 +1,67 @@
---
id: 699b6dd1a2961bd38491ef2e
title: Step 11
challengeType: 1
dashedName: step-11
---
# --description--
The `profile` object had strict keys and types. If you tried to add a property that wasn't defined, or assign an incompatible type, TypeScript would throw an error.
It would be nice if the `userRoles` object wasn't as strict and had more flexible keys.
As you recall in the prior lessons, you can achieve this result by using the `Record` type:
```ts
const studentGrades: Record<string, number> = {
Math: 95,
English: 88,
Science: 92
};
```
In this `studentGrades` example, TypeScript doesn't need to know all of the subjects upfront. This allows you to add more subjects like `Art` or `Music` later on. TypeScript only cares that the values for each key is a number.
Update your `userRoles` object by adding an inline `Record` type. Set it to `<string, string>` so that each key can be any string, and each value must be a string.
# --hints--
Your `userRoles` object should have an inline `Record` type set to `Record<string, string>`.
```js
const explorer = await __helpers.Explorer(code);
const { userRoles } = explorer.variables;
assert.isTrue(userRoles.annotation.matches("Record<string, string>"));
```
# --seed--
## --seed-contents--
```ts
const profile: {
username: string;
age: number;
isLoggedIn: boolean;
bio?: string;
programmingLanguages: string[];
} = {
username: "codeLearner",
age: 25,
isLoggedIn: false,
programmingLanguages: ["JavaScript", "Python", "C++"]
};
console.log(profile);
--fcc-editable-region--
const userRoles = {
--fcc-editable-region--
admin: "full-access",
editor: "limited-access",
viewer: "read-only"
};
console.log(userRoles);
```
@@ -0,0 +1,80 @@
---
id: 699b70b10ae344c802d106b0
title: Step 12
challengeType: 1
dashedName: step-12
---
# --description--
Now it is time to add a couple more properties to the `userRoles` object.
Add a `moderator` property with a value of `"medium-access"`.
Then below that, add a `guest` property with a value of `3`.
# --hints--
Your `userRoles` object should have a `moderator` property.
```js
const explorer = await __helpers.Explorer(code);
const { userRoles } = explorer.variables;
assert.exists(userRoles.objectProps.moderator);
```
Your `moderator` property should have a value of `"medium-access"`.
```js
const explorer = await __helpers.Explorer(code);
const { userRoles } = explorer.variables;
assert.isTrue(userRoles.objectProps.moderator.value.matches("'medium-access'"));
```
Your `userRoles` object should have a `guest` property.
```js
const explorer = await __helpers.Explorer(code);
const { userRoles } = explorer.variables;
assert.exists(userRoles.objectProps.guest);
```
Your `guest` property should have a value of the number `3`.
```js
const explorer = await __helpers.Explorer(code);
const { userRoles } = explorer.variables;
assert.isTrue(userRoles.objectProps.guest.value.matches("3"));
```
# --seed--
## --seed-contents--
```ts
const profile: {
username: string;
age: number;
isLoggedIn: boolean;
bio?: string;
programmingLanguages: string[];
} = {
username: "codeLearner",
age: 25,
isLoggedIn: false,
programmingLanguages: ["JavaScript", "Python", "C++"]
};
console.log(profile);
--fcc-editable-region--
const userRoles: Record<string, string> = {
admin: "full-access",
editor: "limited-access",
viewer: "read-only"
};
--fcc-editable-region--
console.log(userRoles);
```
@@ -0,0 +1,90 @@
---
id: 699b71d98c820ac7ab9eea2a
title: Step 13
challengeType: 1
dashedName: step-13
---
# --description--
If you open the console again, you will see a new error message:
```md
Type 'number' is not assignable to type 'string'.
```
This is happening because you are attempting to add a `guest` property with a number value.
To remove the TypeScript error, change the `guest` property value to `"read-only"`.
With that last change, your user profile workshop is complete!
# --hints--
Your `guest` property for the `userRoles` object should have the value of `"read-only"`.
```js
assert.propertyVal(userRoles, "guest", "read-only");
```
# --seed--
## --seed-contents--
```ts
const profile: {
username: string;
age: number;
isLoggedIn: boolean;
bio?: string;
programmingLanguages: string[];
} = {
username: "codeLearner",
age: 25,
isLoggedIn: false,
programmingLanguages: ["JavaScript", "Python", "C++"]
};
console.log(profile);
const userRoles: Record<string, string> = {
admin: "full-access",
editor: "limited-access",
viewer: "read-only",
moderator: "medium-access",
--fcc-editable-region--
guest: 3
--fcc-editable-region--
};
console.log(userRoles);
```
# --solutions--
```ts
const profile: {
username: string;
age: number;
isLoggedIn: boolean;
bio?: string;
programmingLanguages: string[];
} = {
username: "codeLearner",
age: 25,
isLoggedIn: false,
programmingLanguages: ["JavaScript", "Python", "C++"]
};
console.log(profile);
const userRoles: Record<string, string> = {
admin: "full-access",
editor: "limited-access",
viewer: "read-only",
moderator: "medium-access",
guest: "read-only"
};
console.log(userRoles);
```
@@ -0,0 +1,24 @@
{
"isUpcomingChange": true,
"dashedName": "workshop-type-safe-user-profile",
"helpCategory": "JavaScript",
"blockLayout": "challenge-grid",
"challengeOrder": [
{ "id": "699b4e284291adcfcb90df47", "title": "Step 1" },
{ "id": "699b5370a43e299b4673bcaa", "title": "Step 2" },
{ "id": "699b58c5abecfda9dc81c4ec", "title": "Step 3" },
{ "id": "699b5b7cb68b6d35afca61b7", "title": "Step 4" },
{ "id": "699b61ca8c9cb9bc1968364e", "title": "Step 5" },
{ "id": "699b62c99ec8c63470313365", "title": "Step 6" },
{ "id": "699b64096301fd75e3efeb1b", "title": "Step 7" },
{ "id": "699b65caf17a8e8d340af27d", "title": "Step 8" },
{ "id": "699b68f94a0d154c2a7a5ce0", "title": "Step 9" },
{ "id": "699b6d91cde15459d3619c4a", "title": "Step 10" },
{ "id": "699b6dd1a2961bd38491ef2e", "title": "Step 11" },
{ "id": "699b70b10ae344c802d106b0", "title": "Step 12" },
{ "id": "699b71d98c820ac7ab9eea2a", "title": "Step 13" }
],
"blockLabel": "workshop",
"usesMultifileEditor": true,
"hasEditableBoundaries": true
}
@@ -91,6 +91,7 @@
"comingSoon": true,
"blocks": [
"lecture-introduction-to-typescript",
"workshop-type-safe-user-profile",
"lecture-understanding-type-composition",
"lecture-working-with-generics-and-type-narrowing",
"lecture-working-with-typescript-configuration-files",