Challenges for Typescript
Introduction
This is a list of challenges to make a type the supports a question. challenges. This was very difficult for me to understand, maybe dyslexia, maybe just me but what they are looking for is a type which supports the line highlighted.
Example 1
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
Example Left Side (Signature)
So in this example we are look for a type on the left which = MyPick<Todo, 'title' | 'completed'>. Todo is a Type and 'title' and 'completed' are property keys of that type. So maybe we look at this like a function. So the function signature we need to capture the Type and the keys.
type MyPick<T, K extends keyof T>
So T is the Type and K = keyof T will return a union of keys from T
type myType1 = {
key1: string,
key2: string,
key3: string
}
type K = keyof myType1 // Returns type with values 'key1', 'key2' nad 'key3'
// So we can we can assigned these values
const myVar1: K = 'key1'
const myVar2: K = 'key2'
const myVar3: K = 'rubbish' // Error
Right Side (Implementation)
So we need to make type
MyPick<T, K extends keyof T>
Equal
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
So we need to make an object that is a list (array) of keys title,completed and the a list (array) of values associated with that key.
{
array of keys : value for key for this type
keys[]: values[]
}
So give we have T as the type, K as the key we can do this
{
[k in K] : T[k]
}
Example 2 Iterating over Keys
Ok example 2 is to make a type which makes the all properties read-only
interface Todo {
title: string
description: string
}
const todo: MyReadonly<Todo> = {
title: "Hey",
description: "foobar"
}
So the mistake I made with this was to think they wanted what they wanted before and so this would be the answer.
{
readonly [k in K] : T[k]
}
But this is wrong because the challenges tell you the arguments and in this challenge there is only one argument - the type, no list of keys. Read the question. Going with the left and the right
Example Left Side (Signature)
Define a type of name MyRead=Only and one argument not two.
type MyReadonly<T> =
Right Side (Implementation)
So the right-hand side is the same as the previous question except we do not have K. We don't care because we want all of the keys of type so.
{
readonly [P in keyof T] : T[P]
}
Example 3 Iterating over Arrays transformation
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
type result = TupleToObject<typeof tuple> // expected { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}
type TupleToObject<T extends readonly any[]> = any
Example Left Side (Signature)
So in this case the left hand side is almost provided see below
type TupleToObject<T extends readonly any[]> =
Right Side (Implementation)
We need to, as ever identify the pattern which is result = array[0]: array[0], array[1]: array[1] ... array[n]: array[n]. So how do we access an array. We use an index. So
{
[P in T[number]] : P
}
Improving
However in the challenge they have a test for a invalid assignment
type error = TupleToObject<[[1, 2], {}]>
We need to stop the any in the left hand side. So lets put on some constraints. We constrain it to be an array.
type TupleToObject<T extends readonly (string | symbol | number)[] > = { [P in T[number]] : P }
Example 4 First Conditional Type
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]
type arr3 = []
type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3
type head3 = First<arr3> // expected to be never
type First<T extends any[]> = any
Example Left Side (Signature)
So left side was provided. In the examples we could be more specific to specify string | number for the array but we will see why not in the right hand side
type First<T extends any[]>
Right Side (Implementation)
This is the first conditional type. For this we use the extends keyword which is in the form of SomeType extends OtherType ? x : y and defined by
If SomeType extends another given type OtherType, then ConditionalType is TrueType, otherwise it is FalseType
This confused me at first as arr3 = [] is true and arr2 = [3, 2, 1] is false. But this is because we are actually comparing
type answer1 = [] extends [] ? 'true' : 'false' // true
type answer2 = [] extends never[] ? 'true' : 'false' // true
type answer3 = [1,2,3] extends number[] ? 'true' : 'false' // true
type answer4 = [1,2,3] extends [] ? 'true' : 'false' // false
So this is why this works. I am a better type script person for it. And the answer there is
T extends [] ? never : T[0]
Example 5 typeof
I do not know how this one works.
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
type Length<T> = any
Example Left Side (Signature)
This seemed easy and correct but it is the test cases I struggled with
type Length<T extends []>
Right Side (Implementation)
This is what I came up with but the subsequent test cases made it not the right answer.
type Length<T extends string[]> = T['length']
Answer
So the tests cases which failed were
Expect<Equal<Length<typeof tesla>, 4>>,
Expect<Equal<Length<typeof spaceX>, 5>>,
Clearly we need to make the typeof bit work. So it appears that adding the readonly makes the type the first element in the array.
type Length<T extends readonly any[]> = T['length']
Example 6 Conditional Type 2
This one is where I hoped to answer my first one without looking but sadly not
type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
type MyExclude<T, U> = any
But this is asking can you output types T which are not in U. We need to remember never mean omit
type MyExclude<T, U> = T extends U ? never : T
Example 7 Infer
Infer allows you to name the thing you extracted and is used with conditional types.
type ExampleType = Promise<string>
type Result = MyAwaited<ExampleType> // string
// Make a type for this
type MyAwaited<T> = any
// Which solves the following
type X = Promise<string>
type Y = Promise<{ field: number }>
type Z = Promise<Promise<string | number>>
type Z1 = Promise<Promise<Promise<string | boolean>>>
type T = { then: (onfulfilled: (arg: number) => any) => any }
type cases = [
Expect<Equal<MyAwaited<X>, string>>,
Expect<Equal<MyAwaited<Y>, { field: number }>>,
Expect<Equal<MyAwaited<Z>, string | number>>,
Expect<Equal<MyAwaited<Z1>, string | boolean>>,
Expect<Equal<MyAwaited<T>, number>>,
]
This might get tricky as the answer came from [youtube]. So the first thing was to identify that from the tests they all take a generic parameter (X, Y, Z, Z1 and T). Looking at the types they are all Promises (except T) so lets make the generic and unknown promise
type MyAwaited<T extends Promise<unknown>> = any
Now we can use the infer keyword. This will solve the first two test cases X and Y.
type MyAwaited<T extends Promise<unknown>> = T extends Promise<infer V> ? V : never
For the next two cases Z and Z1 this is not resolved because they unwrap promises within promises, the first one promise, the second two promises, luckily we have type to unwrap a promise so we can call this recursively. We check if the value returned from the inter is a Promise<unknown>, if it is we use our type to remove the Promise, if it isn't we return our value.
type MyAwaited<T extends Promise<unknown>> =
T extends Promise<infer V> ?
(V extends Promise<unknown> ? MyAwaited<V>: V) :
never
I could not solve the final test but in the end found an answer where they used the recursive calling of then.
type MyAwaited<T> = T extends { then: (onfulfilled: (arg: infer U) => unknown) => unknown; }
? MyAwaited<U>
: T;
Would have like to have seen the equivalent solution for this.
Example 8 Recursion II
This is apparently easy. So depressed doing these but youtube had the answer [youtube] and explained here because I need it explaining. This was the question, write an includes function.
type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`
I did actually know the answer for the first attempt but my it was not enough.
type Includes<T extends any[], U> = U extends T[number] ? true: false
// Many of the tests passed
Expect<Equal<Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Kars'>, true>>,
Expect<Equal<Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'>, false>>,
Expect<Equal<Includes<[1, 2, 3, 5, 6, 7], 7>, true>>,
Expect<Equal<Includes<[1, 2, 3, 5, 6, 7], 4>, false>>,
Expect<Equal<Includes<[1, 2, 3], 2>, true>>,
Expect<Equal<Includes<[1, 2, 3], 1>, true>>,
// But many did not
Expect<Equal<Includes<[{}], { a: 'A' }>, false>>,
Expect<Equal<Includes<[boolean, 2, 3, 5, 6, 7], false>, false>>,
Expect<Equal<Includes<[true, 2, 3, 5, 6, 7], boolean>, false>>,
Expect<Equal<Includes<[{ a: 'A' }], { readonly a: 'A' }>, false>>,
Expect<Equal<Includes<[{ readonly a: 'A' }], { a: 'A' }>, false>>,
Expect<Equal<Includes<[1], 1 | 2>, false>>,
Expect<Equal<Includes<[1 | 2], 1>, false>>,
The issue is with how to compare. If we look at this test case, test is equal to true and therefore fails.
type test = Includes<[boolean, 2, 3, 5, 6, 7], false> // true
We need to use something better to compare. He googled and found this
type test2 = Equal<boolean, false> // false
So now all we need to do is to iterate using our new approach to compare. But first a reminder. With type script we can obtain the first or second element and the rest using the spread operator. This is the same for types
let [first, ...rest] = [1, 2, 3, 4];
let [first,second ...rest] = [1, 2, 3, 4];
So what we do below is we separate the first element in First off from the Rest of the elements. If equal all good, if not call recursively with the rest until there are no more elements. One last thing, the youtuber also thought this was not a simple challenge
type Includes<T extends any[], U> = T extends [infer First, ...infer Rest] ?
(Equal<U, First> extends true ? true : Includes<Rest, U>) : false
Example 9 Parameters
So Given
const foo = (arg1: string, arg2: number): void => {}
const bar = (arg1: boolean, arg2: { a: 'A' }): void => {}
const baz = (): void => {}
const buz = (arg1: number): number => { return 0}
type cases = [
Expect<Equal<MyParameters<typeof foo>, [string, number]>>,
Expect<Equal<MyParameters<typeof bar>, [boolean, { a: 'A' }]>>,
Expect<Equal<MyParameters<typeof baz>, []>>,
Expect<Equal<MyParameters<typeof buz>, [number]>>,
]
// Solve
type MyParameters<T extends (...args: any[]) => any> = any
So we are passing (): void => {} so we should be able to capture the input as (...args: T) => any
type MyParameters<T> = T extends ( (...args: infer U) => any ) ? U : never
Example 9 Parameters
So Given
const foo = (arg1: string, arg2: number): void => {}
const bar = (arg1: boolean, arg2: { a: 'A' }): void => {}
const baz = (): void => {}
const buz = (arg1: number): number => { return 0}
type cases = [
Expect<Equal<MyParameters<typeof foo>, [string, number]>>,
Expect<Equal<MyParameters<typeof bar>, [boolean, { a: 'A' }]>>,
Expect<Equal<MyParameters<typeof baz>, []>>,
Expect<Equal<MyParameters<typeof buz>, [number]>>,
]
// Solve
type MyParameters<T extends (...args: any[]) => any> = any
So we are passing (): void => {} so we should be able to capture the input as (...args: T) => any
type MyParameters<T> = T extends ( (...args: infer U) => any ) ? U : never
Example 10 Return Value
Possibly getting easier. Copying is getting quicker and now I used my learning today so not a waste.
const fn = (v: boolean) => {
if (v)
return 1
else
return 2
}
type MyReturnType<T> = any
type cases = [
Expect<Equal<string, MyReturnType<() => string>>>,
Expect<Equal<123, MyReturnType<() => 123>>>,
Expect<Equal<ComplexObject, MyReturnType<() => ComplexObject>>>,
Expect<Equal<Promise<boolean>, MyReturnType<() => Promise<boolean>>>>,
Expect<Equal<() => 'foo', MyReturnType<() => () => 'foo'>>>,
Expect<Equal<1 | 2, MyReturnType<typeof fn>>>,
Expect<Equal<1 | 2, MyReturnType<typeof fn1>>>,
]
If we look at the examples for the test cases and switching them around we see a pattern
MyReturnType<() => string
MyReturnType<() => 123
MyReturnType<() => Promise<boolean>
MyReturnType<() => () => 'foo'
MyReturnType<typeof fn>
MyReturnType<typeof fn1>
The last two look slightly different but they are not. typeof fn just means
fn() => number
fn1() => number
So the answer is
type MyReturnType<T extends Function> =
T extends (...args: any) => infer R
? R
: never
Example 11 MyReadonly2
interface Todo1 {
title: string
description?: string
completed: boolean
}
interface Todo2 {
readonly title: string
description?: string
completed: boolean
}
interface Expected {
readonly title: string
readonly description?: string
completed: boolean
}
Expect<Alike<MyReadonly2<Todo1>, Readonly<Todo1>>>,
Expect<Alike<MyReadonly2<Todo1, 'title' | 'description'>, Expected>>,
Expect<Alike<MyReadonly2<Todo2, 'title' | 'description'>, Expected>>,
Expect<Alike<MyReadonly2<Todo2, 'description' >, Expected>>,
type MyReadonly2<T, K> = any
This was though. The first thing to notice is the first test case. For this we need to remember we can have default arguments in typescript
type MyReadonly2<T, K extends keyof T = keyof T> = any
Next we need to understand intersection which is the & sign. This will combine the sets in the braces.
type MyReadonly2<T, K extends keyof T = keyof T> =
{ SET A } &
{ SET B }
Set A is all of the keys specified.
type MyReadonly2<T, K extends keyof T = keyof T> =
{ readonly [P in K] : T[P] } &
{ SET B }
Set B is just excluding the value which are not in K. This is the same as the Omit. Understand the Mapped As and this is easy
type MyReadonly2<T, K extends keyof T = keyof T> =
{ readonly [P in K] : T[P] } &
{ [P in keyof T as P extends K ? never : P]: T[P] }
Example 12 Deep Readonly and Recursion
This was going so well as I understood recursion but needed something like
{ if !object ? readonly [P in keyof T]: DeepReadonly<T[P]> : DeepReadonly< T[P]> }
type X1 = {
a: () => 22
b: string
c: {
d: boolean
e: {
g: {
h: {
i: true
j: 'string'
}
k: 'hello'
}
l: [
'hi',
{
m: ['hey']
},
]
}
}
}
type Expected1 = {
readonly a: () => 22
readonly b: string
readonly c: {
readonly d: boolean
readonly e: {
readonly g: {
readonly h: {
readonly i: true
readonly j: 'string'
}
readonly k: 'hello'
}
readonly l: readonly [
'hi',
{
readonly m: readonly ['hey']
},
]
}
}
}
So the extends never says is it not enumerable so we have out solution almost...
type DeepReadonly<T> = keyof T extends never
? T // Object
: { readonly [k in keyof T]: DeepReadonly<T[k]> }; // Not an Object
Almost because of course there are many things we can put in an object, functions or maybe we have not an object but a union
type X2 = { a: string } | { b: number }
type Expected2 = { readonly a: string } | { readonly b: number }
Like a lot of these you can go on youtube and get the answer from Michigan Typescript. They actually used the answer above but below is what solved this completely for me. So the author decided to look at the value not the key. i.e. the right hand side. If this is not enumerable
type DeepReadonly<T> = {
readonly [P in keyof T]:
keyof T[P] extends never
? T[P]
: DeepReadonly<T[P]>
}
Example 13 Chainable
It seems this is more like doing cryptic crosswords where I need to break them down to even understand the question.
const result = config
.option('foo', 123)
.option('name', 'type-challenges')
.option('bar', { value: 'Hello World' })
.get()
// expect the type of result to be:
interface Result {
foo: number
name: string
bar: {
value: string
}
}
I guess the first thing is to noticed is we are dealing with
const config = T
.(Key,Value): T
.(Key,Value): T
.(Key,Value): T
.get() : T
Next we need to look at the args which are Key and Value. This gave the following
type Chainable<T = {}> = {
option<
K extends PropertyKey,
V
>(
): Chainable<someKey, someValue>
get(): T
}
So still a bit of voodoo but here goes. Here how we get the key and value
key: K extends keyof T
? V extends T[K]
? never
: K
: K,
value: V
So this is the first attempt but not quite perfect
type Chainable<T = {}> = {
option<K extends string, V>(
key: K,
value: V
): Chainable<Omit<T,K> & Record<K, V>>;
get(): T;
};
And the final answer is
type Chainable<T = {}> = {
option<
K extends PropertyKey,
V>(
key: K extends keyof T
? V extends T[K]
? never
: K
: K,
value: V
): Chainable<Omit<T,K> & Record<K, V>>;
get(): T;
};
This still fails this to error for this
const result3 = a
.option('name', 'another name')
// @ts-expect-error
.option('name', 123)
.get()
Example where we have no arguments and use value of key
The important thing is different arguments and different return arguments is a functional override. Same return arguments is a union of arguments.
import { Equal, Expect } from "../helpers/type-utils";
const obj = {
a: 1,
b: 2,
c: 3,
} as const;
type ObjKey = keyof typeof obj;
function getObjValue(): 1;
function getObjValue<TKey extends ObjKey>(key: TKey): (typeof obj)[TKey];
function getObjValue(key: ObjKey = "a") {
return obj[key];
}
const one = getObjValue("a");
const oneByDefault = getObjValue();
const two = getObjValue("b");
const three = getObjValue("c");
type tests = [
Expect<Equal<typeof one, 1>>,
Expect<Equal<typeof oneByDefault, 1>>,
Expect<Equal<typeof two, 2>>,
Expect<Equal<typeof three, 3>>
];
Example where we detect to number of arguments passed
import { it } from "vitest";
interface Events {
click: {
x: number;
y: number;
};
focus: undefined;
}
export const sendEvent = <TEventKey extends keyof Events>(
event: TEventKey,
...args: Events[TEventKey] extends undefined
? []
: [payload: Events[TEventKey]]
) => {
// Send the event somewhere!
};
it("Should force you to pass a second argument when you choose an event with a payload", () => {
// @ts-expect-error
sendEvent("click");
sendEvent("click", {
// @ts-expect-error
x: "oh dear",
});
sendEvent(
"click",
// @ts-expect-error
{
y: 1,
},
);
sendEvent("click", {
x: 1,
y: 2,
});
});
it("Should prevent you from passing a second argument when you choose an event without a payload", () => {
sendEvent("focus");
sendEvent(
"focus",
// @ts-expect-error
{},
);
});
This to me comes under the how clever am I category. I cannot see myself doing this but here goes for the explanation. The original problem was given as
export const sendEvent = (event: keyof Events, ...args: any[]) => {
// Send the event somewhere!
};
And the answer as
export const sendEvent = <TEventKey extends keyof Events>(
event: TEventKey,
...args: Events[TEventKey] extends undefined
? []
: [payload: Events[TEventKey]]
) => {
What we are doing is
- first extracting the keyof to a generic.
- second optionally looking to see if the arguments extend undefined, if it does then we know we have "focus"
- if it does then we have a tuple of arguments, we name the tuple for the click event playload for convenience