Type Challenges
Link: https://github.com/type-challenges/type-challenges
Easy
4 - Pick
- The goal of the exercise is to implement the built-in
Pick<T, K>
generic without using it. It constructs a type by picking the set of propertiesK
fromT
.
// This is what I have to solve:
type MyPick<T, K> = any;
// These are my given interfaces
interface Todo {
title: string;
description: string;
completed: boolean;
}
interface Expected1 {
title: string;
}
interface Expected2 {
title: string;
completed: boolean;
}
// These are the cases that I need to get to pass
type cases = [
Expect<Equal<Expected1, MyPick<Todo, "title">>>,
Expect<Equal<Expected2, MyPick<Todo, "title" | "completed">>>,
// @ts-expect-error
MyPick<Todo, "title" | "completed" | "invalid">
];
- As just a place to start, look at the test cases. So
Expected1
is the resulting type that you're trying to get. You are trying to get it via your custom implementation ofPick
viaMyPick
. So whenMyPick
is good, tests pass and the type errors go away. - If you compare
Todo
vs.Expected1
,Todo
has three properties:title
,description
, andcompleted
.Expected1
only hastitle
. So fromTodo
, the property that I want to pick is justtitle
. And that test case is already done for me:Expect<Equal<Expected1, MyPick<Todo, 'title'>>>
. To write this in a kind of pseudo-code, it would look like:Expected1 === MyPick(Todo, 'title')
- my first argument is the object I want to peel properties from and then the rest of the arguments are the keys of that object that I want. - Same thing with
Expected2
- I want to peel off properties fromTodo
, this time I wanttitle
andcompleted
. - This is a good starting context - I can see that
Expected1
andExpected2
are just interfaces that have only some of the properties ofTodo
. Now I just need to write the code that solves this issue. - To try to accomplish this, from scratch, the concepts used here is generics or generic types.
- CM: This honestly feels way more complicated than it needs to be....
- Here is the answer and then I will try to explain it:
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
- If I were to read this code aloud, it would sound like this:
The type
MyPick
takes two type parameters,T
andK
whereT
represents an object type andK
is a union of keys ofT
(in other words,K
can have every key ofT
or only some, it cannot have keys that aren't present inT
).MyPick
uses a mapped type to create a new type (again, that can only be a subset of the properties inT
). For each keyP
inK
, create a new propertyP
in the output type whose value is the corresponding property ofT
. The resulting type is{ [P in K]: T[P] }
. - Here is a breakdown of the code:
K extends keyof T
ensures thatK
is a union of keys ofT
.[P in K]
is a mapped type that iterates over each key inK
.T[P]
is the type of the property inT
corresponding to the current keyP
.
- I rewrote it using longer form variable names to kind of explain what's happening:
type MyPick<BaseObject, NewType extends keyof BaseObject> = {
[Property in NewType]: BaseObject[Property];
};
-
Here,
BaseObject
is the object likeTodo
that I'm using as my guide. I can only use the properties from myBaseObject
. -
And then
NewType
is the new type that I'm creating. I useextends keyof
which is a type of contraint that restricts a type parameter to be a union of keys of another type. So myNewType
can ONLY contain the properties thatBaseObject
has -
And now, in my
NewType
object, for each keyProperty
inNewType
, create a new property (Property
) in the output type whose value is corresponding property ofBaseObject
. -
Concepts:
- generics
- mapped types
7 - Readonly
- Here is the challenge:
interface Todo {
title: string;
description: string;
}
const todo: MyReadonly<Todo> = {
title: "Hey",
description: "foobar",
};
todo.title = "Hello"; // Error: cannot reassign a readonly property
todo.description = "barFoo"; // Error: cannot reassign a readonly property
- and here is the answer in both TS lingo and long form
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};
type MyReadonly<TargetObject> = {
readonly [Property in keyof TargetObject]: TargetObject[Property];
};
- the "secret" here is the
readonly
keyword. - All of this stuff looks like a different language but from a code perspective, if I had to implement this, I would essentially say: "iterate through the object and make each property readonly"...I just have no idea how to do that in TypeScript
- The answer is easy to understand but just to explain it, my
MyReadonly
type takes a generic typeT
and then returns a new type that is a mapped type. The mapped type iterates over each keyP
inT
and creates a new propertyP
in the output type whose value is the corresponding property ofT
. The only thing that makes itreadonly
is the first word in the type:readonly [P in keyof T]
. - In the type below, I'm removing the
readonly
.
type CoolObject<T> = {
[P in keyof T]: T[P];
};
- So type
CoolObject
just creates a shadow copy and adds no additional properties or modifications. - Sticking with the
CoolObject
type, let's say that I want to make everything else editable EXCEPT thename
property:
type CoolObject<T> = {
readonly [P in keyof T]: P extends "name" ? readonly T[P] : T[P];
};
- And here is an opposite example where everything is
readonly
except for an object'sbalance
property:
type CoolObject<T> = {
readonly [P in keyof T]: P extends "balance" ? T[P] : readonly T[P][];
};
11 - Tuple to Object
- Here is the challenge:
const tuple = ["tesla", "model 3", "model X", "model Y"] as const;
const result: TupleToObject<typeof tuple>; // expected { tesla: 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}
- and here is the answer:
const tuple = ["tesla", "model 3", "model X", "model Y"] as const;
// CORRECT: working answer
type TupleToObject<T extends readonly any[]> = {
[P in T[number]]: P;
};
- My gut here was just this:
// WRONG
type TupleToObject<T extends readonly any[]> = {
[P in T]: P;
};
- but that doesn't work. It doesn't work because you're using the type
T
as a key. But you don't want the entire type, you just want the value from the tuple as a key. - So the next logical choice, IMO, is to use
[P in T[string]]: P
but that doesn't work either...
// WRONG
type TupleToObject<T extends readonly any[]> = {
[P in T[string]]: P;
};
- In TypeScript,
T[string]
refers to the type of the value that would be accessed by using a string as an index on the typeT
. This is typically used whenT
is an object type, andT[string]
represents the type of the property value associated with the string index key. - However, in the case of tuples, the index type
T[number]
is used to represent the union of all element types in the tuple. It extracts the individual types of the elements rather than the specific values associated with them. Tuples are arrays! We can't use a string to access something on an array. - Since we want to iterate over the elements of the tuple and use each element as a property
key, the correct approach is to use
T[number]
as the index type. This allows us to access and iterate over the union of all element types in the tupleT
and use them as property keys in the resulting object type. - That's how we get to the final answer:
type TupleToObject<T extends readonly any[]> = {
[P in T[number]]: P;
};
- I'd like to take another crack at explaining this to myself. The
T
variable has always confused me. It kind of makes sense but I think it's important to remember the context of the type. In this case, we are given a tuple which is anarray
. SoT
is the array andT[number]
is the value at that place in the array. If it was a constant,T[0]
would have some value liketesla
, for example. So keeping that in mind, let's go:- what am I trying to do? I'm trying to convert a tuple to an object such that
['boy', 'fish']
becomes{ boy: 'boy', fish: 'fish' }
. - If I was making an object, I could just iterate through the array. But given that I need to
use indexed access, I need to use
T[number]
. - So
P
as in "property" in my tupleT
at position[number]
, or,T[number]
is equal toP
.
- what am I trying to do? I'm trying to convert a tuple to an object such that
14 - First of Array
- Here is the challenge:
// Implement a generic `First<T>` that takes an Array `T` and returns it's first element's type.
type arr1 = ["a", "b", "c"];
type arr2 = [3, 2, 1];
type head1 = First<arr1>; // expected to be 'a'
type head2 = First<arr2>; // expected to be 3
- First, I have no idea where to even start. My gut was maybe to try Extract but I have no idea how it actually works on an array.
- this doesn't work:
// Doesn't work
type First<T extends any[]> = Extract<T, T[0]>;
- here is the answer:
// Answer
type First<T extends any[]> = T extends [] ? never : T[0];
- Without digging into the answer, let's take a step back. Let's pretend that I could assume that the array always has at least one element. Then I could just do this:
// Returns the first element of an array IF there is at least one element
type First<T extends any[]> = T[0];
// Test Cases
type cases = [
Expect<Equal<First<[3, 2, 1]>, 3>>, // Pass
Expect<Equal<First<[() => 123, { a: string }]>, () => 123>>, // Pass
Expect<Equal<First<[]>, never>>, // FAIL
Expect<Equal<First<[undefined]>, undefined>> // Pass
];
- The only one that fails is the empty one. So now I need to check for that. If I was using
JavaScript, I could just do
T.length === 0
but I can't. - This is where the
extends
keyword comes in. I can use it to check for an empty array:
// Returns the first element of an array IF there is at least one element
type First<T extends any[]> = T extends [] ? never : T[0];
- The
extends
keyword is used to check if a type is a subtype of another type. In this case, I'm checking ifT
is a subtype of[]
. If it is, then returnnever
(which is a type that represents the type of values that never occur). If it's not, then return the first element - I think that what is important is that
[]
is not just a stand-in for the wordArray
. It's specifically checking if the type,T
, is not just an array but an empty array. - Consider this code:
// Doesn't work AND returns this error:
// Generic type 'Array<T>' requires 1 type argument(s).(2314)
type First<T extends any[]> = T extends Array ? never : T[0];
- This doesn't work because
Array
is a generic type. It requires a type argument. So I'm not even usingArray
properly here. - Here is another solution using
Extract
but it doesn't look like it handles empty arrays:
// Works
type First<T extends any[]> = T[Extract<keyof T, "0">];
// It seems to pass the test case with the empty array but I don't understand why
- Here is another one that works that I think makes sense:
type First<T extends any[]> = T["length"] extends 0 ? never : T[0];
- exactly as it says, we're accessing the
length
property on the array and checking if it's0
. If it is, returnnever
. If it's not, return the first element.
18 - Length of Tuple
- Here is the challenge:
// For given a tuple, you need create a generic `Length`, pick the length of the tuple
type tesla = ["tesla", "model 3", "model X", "model Y"];
type spaceX = [
"FALCON 9",
"FALCON HEAVY",
"DRAGON",
"STARSHIP",
"HUMAN SPACEFLIGHT"
];
type teslaLength = Length<tesla>; // expected 4
type spaceXLength = Length<spaceX>; // expected 5
- for something like this, I again, don't know where to start - I don't understand the point of
this. I need to create a type,
Length
, that when given a tuple, returns the length of that tuple. That sounds like a function. - this is the code that I need to get working:
type Length<T> = any;
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from "@type-challenges/utils";
const tesla = ["tesla", "model 3", "model X", "model Y"] as const;
const spaceX = [
"FALCON 9",
"FALCON HEAVY",
"DRAGON",
"STARSHIP",
"HUMAN SPACEFLIGHT",
] as const;
type cases = [
Expect<Equal<Length<typeof tesla>, 4>>,
Expect<Equal<Length<typeof spaceX>, 5>>,
// @ts-expect-error
Length<5>,
// @ts-expect-error
Length<"hello world">
];
- here is one of the most popular answers;
// Answer
type Length<T extends readonly any[]> = T["length"];
- I don't know if this is the proper "frame" to adopt but I am going to try to explain it in a
way that makes sense to me. So first, I started with:
type Length<T> = any
. It doesn't specify anything really aboutT
unlike some of the previous tuple examples. So one assumption I had about these challenges is that the left side type is "given" is wrong - I can change what I need. - Second, let's look at what's in the
<>
;<T>
. First thing about this challenge is that I am trying to figure out the length of a tuple (array) so if I wanted to tighten up the current type forT
, I could do this:<T extends any[]>
. This is saying thatT
is an array of any type. - Once I do that, I can access the
length
property on the array (in the way I'd access a property of an object) by doingT['length']
. So now I havetype Length<T extends any[]> = T['length']
. - In TypeScript, I see that this doesn't work. And this is another key item that I'm learning - TypeScript gives you the answer to your issue...in the challenge, it said for the one test case: "The type 'readonly ["tesla", "model 3", "model X", "model Y"]' is 'readonly' and cannot be assigned to the mutable type 'any[]'". It's giving me the answer already - I have to make my generic type a readonly array.
- So now when I update it to
type Length<T extends readonly any[]> = T['length']
, it works.
Early Take-aways (that may or may not be right)
- The left side type, in the brackets (
<>
), defines the type you are working with on the right side. So Step 1 should be to properly define it. If you are creating a type for an object, make sure you define it that way. - TypeScript will give you the answer to your issue. If your type isn't working, read what the error is - it's telling you why it doesn't like it.
- Access native properties of an array like
T['length']
orT[0]
- Things can be "defined" in the definition. If you look at the Tuple To Object
challenge,
[P in T[number]]: P
, I am essentially defining "P" in the definition. I am saying the property,P
, that is atT[number]
, is being set toP
. - All of this "knowledge" is temporary until I actually learn this shit.
43 - Exclude
- here is the challenge:
// Implement the built-in Exclude<T, U>
// => Exclude from T those types that are assignable to U
type Result = MyExclude<"a" | "b" | "c", "a">; // 'b' | 'c'
- and here is the setup:
type MyExclude<T, U> = any;
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<MyExclude<"a" | "b" | "c", "a">, "b" | "c">>,
Expect<Equal<MyExclude<"a" | "b" | "c", "a" | "b">, "c">>,
Expect<
Equal<MyExclude<string | number | (() => void), Function>, string | number>
>
];
- here is another where I have no idea how to do it. So when I look at the test cases, I will label
my
T
andU
types. For Case 1:T = 'a' | 'b' | 'c'
andU = 'a'
and so the answer I want is'b' | 'c'
. - So when starting with
type MyExclude<T, U> = any
, my gut is always like I want to find out the type of the argument (e.g. an array, an object, whatever) and then iterate through it. - I had no idea how to continue this one so I looked up a few answers and this is what I got:
// Answer
type MyExclude<T, U> = T extends U ? never : T;
- It's almost like it iterates the individual types in
T
and checks if they are assignable toU
. I don't know. Maybe the frame ofT
being an argument isn't quite right... - the code part makes sense and seems intuitive but I don't entirely get it. In my head, it would only
make sense if
T
was a single type not'a' | 'b' | 'c'
.