DEV Community

TK
TK

Posted on • Updated on

Level up on TypeScript! What I learned from type-challenges

Summary of this article

This article introduces type-challenges, which helped me(a TypeScript beginner) to better understand TypeScript.

type-challenges is a collection of TypeScript challenges divided into levels. You can learn while having fun!

In the first half of this article, I will introduce what kind of TypeScript challenges you will face with type-challenges's category "easy".

And also I will introduce what you can learn from them.

In the second half, I will briefly explain the techniques I often used to solve the challenges.

Here is the link to type-challenges.

https://github.com/type-challenges/type-challenges

type-challenge category "easy"

What kind of challenges

There are 13 challenges in total in the category "easy". (at the time of writing)

The challenge is to implement the following utility types.

  • Pick<T, K> utility type (4 - Pick)
  • Readonly<T> utility type (7 - ReadOnly)
  • TupleToObject<T> utility type, which converts an array to an object type, (11 - Tuple to Object)
  • First<T> utility type, which returns the first element of an array (14 - First)
  • length<T> utility type, which returns the length of an array (18 - Length of Tuple)
  • Exclude<T, U> utility type (43 - Exclude)
  • Awaited<T> utility type (189 - Awaited)
  • If<C, T, F> utility type, which returns T if C is truthy, and returnsF if falsy) (268 - If)
  • Type version of Array.concat() (533 - Concat)
  • Type version of Array.includes() (898 - Includes)
  • Type version of Array.push() (3057 - Push)
  • Type version of Array.unshift() (3060 - Unshift)
  • Parameters<T> utility type (3312 - Parameters)

Techniques you can learn

First of all, Generics is used in all challenges, so it is great for those who want to practice Generics.

Below are some of the techniques often used to solve the challenges.

I will only introduce the terminology here and explain them in the following section.

  • Generic Constraints
  • readonly modifier
  • Mapped Types
  • Indexed Access Types
  • Conditional types
    • Filtering by never
    • Type inference with infer
  • Variadic Tuple Types

If you have any terminology that makes you wonder "what the heck is that 🤔", try type-challenges!

Frequently used techniques

Here is a brief description of the commonly used techniques in the challenges of type-challenges category "easy".

This is only a brief explanation, so please check the reference links for details.

Generic Constraints

Generic Constraints is a way to restrict Generics to a specific type using the extends keyword.

This is useful, for example, in the following example.

*Example: arg with type Generics T calls the length method, but since Generics can be arbitrary (it may not have length method), a compile error occurs. *

function checkLength<T>(arg: T): number {
  // Error: Property 'length' does not exist on type 'T'.
  return arg.length;
}
Enter fullscreen mode Exit fullscreen mode

In such cases, the extends keyword can be used to constrain the type of Generics type arguments to a specific type. (Generic Constraints)

This allows you to safely access the properties of an object.

function checkLength<T extends string>(arg: T): number {
  return arg.length;
}
Enter fullscreen mode Exit fullscreen mode

Reference:
https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints

Combination with keyof type operators

Generic Constraints is often used in combination with keyof type operators. (See the reference link below for more information on keyof.)

In the following example, the second argument key of the getProperty function is set to K extends keyof T to restrict the type to accept only those objects of type T (union type of the x object's property key).

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}
// K => "a" | "b" | "c" | "d"

let x = { a: 1, b: 2, c: 3, d: 4 }

getProperty(x, "a");
getProperty(x, "m");
// Error: Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.
Enter fullscreen mode Exit fullscreen mode

Reference:
https://www.typescriptlang.org/docs/handbook/2/keyof-types.html

readonly modifier

The readonly modifier can be used to make arrays or object properties immutable.

Example: assignment to obj foo causes an error.

let obj: {
  readonly foo: number;
};
obj = { foo: 1 }
obj.foo = 2;
// Error: Cannot assign to 'foo' because it is a read-only property.
Enter fullscreen mode Exit fullscreen mode

Example: Assigning to a or changing an element causes an error.

let a = readonly ['a', 'b', 'c'];
a.push('d'); // error
a[0] = 'x'; // error
Enter fullscreen mode Exit fullscreen mode

Reference:
https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#readonly-and-const

Mapped Types

Mapped Types is a technique for creating types by iterating over keys by using the in keyword on a union type.

The following example creates a HelloTranslation object type using the in keyword for the AvialbleLanguage union type.

type AvailableLanguage = 'en' | 'jp';

type HelloTranslation = {
    [key in AvailableLanguage]: string;
}
/**
 * type HelloTranslation = {
 * en: string; }
 * jp: string; }
 * }
 */

const hello: HelloTranslation = {
    'en': 'hello', 
    'jp': 'hello',
    'fr': 'bonjour',
    /**
     * Error: Type '{ en: string; jp: string; fr: string; }' is not assignable to type 'HelloTranslation'.
     * Object literal may only specify known properties, and ''fr'' does not exist in type 'HelloTranslation'.
     */       
}
Enter fullscreen mode Exit fullscreen mode

To map to union types, another technique often used is to use the keyof type operator to turn properties of an object type into union types, and then iterate over them with in.

Reference:
https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#handbook-content

Indexed Access Types

Indexed Access Types are similar to the way JavaScript accesses object properties.

The following example accesses property types of a Cat object type.

type Cat = { age: number; name: string; alive: boolean }
type Age = Cat["age"];
// type Age = number;
Enter fullscreen mode Exit fullscreen mode

The index type to be accessed is itself a type, so you can also use union types to access multiple properties.

type Cat = { age: number; name: string; alive: boolean }

type Type1 = Cat["age" | "name"];  
// type Type Type1 = string | number
type Type2 = Cat[keyof Cat];
// type Type Type2 = string | number | boolean
Enter fullscreen mode Exit fullscreen mode

Another useful technique for Indexed Access Type is to access tuple types with [number].
This allows elements of a tuple type to be retrieved as union types.

type Animals = ['cat', 'dog', 'tiger', 'lion', 'elephant'];

type AnimalsUnion = Animals[number];
// type AnimalsUnion = 'cat' | 'dog' | 'tiger' | 'lion' | 'elephant'
Enter fullscreen mode Exit fullscreen mode

This is a useful technique that is powerful when combined with the Mapped types above because it generates union types.

Reference:
https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html

Conditional types

Conditional types are similar to JavaScript's Ternary operator using extend and ? to create conditional types.

type True = true extends boolean ? true : false;
Enter fullscreen mode Exit fullscreen mode

Reference:
https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#handbook-content

Filtering with never

You can use never, meaning a value that cannot occur, as a return for Conditional types to narrow down the types.

In the following example, elements of union types not in AvailableLanguage are excluded by never.

type AvailableLanguage = 'en' | 'jp';
type FilteredLanguage<T> = T extends AvailableLanguage ? T : never;
type Foo = FilteredLanguage<'en' | 'fr' | 'jp' | 'cn' | 'kr'>; 
// type Foo = "en" | "jp"
Enter fullscreen mode Exit fullscreen mode

Reference:
https://www.typescriptlang.org/docs/handbook/2/functions.html#never

type inference with infer

infer is used with Conditional types to infer the type that matches a condition.
The following example shows how to infer the return type of a function and get its return value with infer if T is a function type.

type GetReturnType<T> = T extends (. .args: unknown[]) => infer Return
  ? Return
  : never;

type Num = GetReturnType<() => number>;
// type Num = number 

type Str = GetReturnType<(x: string) => string>;     
// type Str = string

type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;   
// type Bools = boolean[]
Enter fullscreen mode Exit fullscreen mode

Reference:
https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#inferring-within-conditional-types

Variadic Tuple Types

Variadic Tuple Types are like type versions of JavaScript's spread syntax.

type Numbers = [number, number];
type StrStrNumNumBool = [. .Strings, . .Numbers, boolean];
// type StrStrNumNumBool = [string, string, number, number, boolean]
Enter fullscreen mode Exit fullscreen mode

Reference:
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#variadic-tuple-types

Summary

This article summarizes what you can learn with type-challenges category "easy".

If you are looking for a way to learn TypeScript, I recommend type challenges, a hands-on, fun way to learn!
Each challenge has a well-explained solution page, so give it a shot!

The article is also available in Japanese: https://zenn.dev/takuyakikuchi/articles/19d8769895a863

Top comments (0)