Site logo

Developer Blog

Pavel Koltyshev

Задачи по TypeScript (часть 2)

Содержание

Задания взяты из TypeScript Challenges.

Get Return Type

Реализуйте дженерик ReturnType<T>.

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

const fn = (v: boolean) => {
  if (v) return 1;
  else return 2;
};

type a = MyReturnType<typeof fn>; // should be "1 | 2"

Omit

Реализуйте дженерик Omit<T, K>.

Создает тип, выбирая все свойства из T и затем удаляя K.

type MyOmit<T, K extends keyof T> = { [P in keyof T as P extends K ? never : P]: T[P] };

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = MyOmit<Todo, 'description' | 'title'>;

const todo: TodoPreview = {
  completed: false,
};

Readonly

Реализуйте дженерик MyReadonly<T, K>.

K указывает набор свойств T, для которых должно быть установлено значение “только для чтения”. Если K не указан, все свойства должны быть доступны только для чтения.

type MyReadonly<T, K extends keyof T = keyof T> = Omit<T, K> & { readonly [P in K]: T[P] };

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

const todo: MyReadonly<Todo, 'title' | 'description'> = {
  title: 'Hey',
  description: 'foobar',
  completed: false,
};

todo.title = 'Hello'; // Error: cannot reassign a readonly property
todo.description = 'barFoo'; // Error: cannot reassign a readonly property
todo.completed = true; // OK

Deep Readonly

Реализуйте дженерик DeepReadonly<T> которые делает каждый объект (и его подобъекты рекурсивно) доступным только для чтения.

Не нужно обрабатывать случаи с массивами, функциями, классами и т. д.

type DeepReadonly<T> = { readonly [P in keyof T]: keyof T[P] extends never ? T[P] : DeepReadonly<T[P]> };

type X = {
  x: {
    a: 1;
    b: 'hi';
  };
  y: 'hey';
};

type Expected = {
  readonly x: {
    readonly a: 1;
    readonly b: 'hi';
  };
  readonly y: 'hey';
};

type Todo = DeepReadonly<X>; // should be same as `Expected`

Tuple to Union

Реализуйте дженерик TupleToUnion<T>, который берет значения в кортеже и возвращает объединение его значений.

type TupleToUnion<T extends any[]> = T[number];

type Arr = ['1', '2', '3'];

type Test = TupleToUnion<Arr>; // expected to be '1' | '2' | '3'

Chainable Options

Цепочка вызовов обычно используются в Javascript. Но когда мы перейдем на TypeScript, сможете ли вы его правильно напечатать?

В этом задании вам нужно ввести объект или класс — что угодно — чтобы предоставить два варианта option(key, value) и get(). В option вы можете расширить текущий тип конфигурации с помощью заданного ключа и значения. Нам нужно получить доступ к окончательному результату через get.

Вам не нужно писать какую-либо логику js/ts для решения проблемы — просто на уровне типа.

Условимся что key принимает только string, а value может быть любым. Один и тот же key не будет передан дважды.

type Chainable<T = {}> = {
  option<K extends string, V>(key: K extends keyof T ? never : K, value: V): Chainable<Omit<T, K> & Record<K, V>>;
  get(): T;
};

declare const config: Chainable;

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;
  };
}

Last of Array

Реализуйте дженерик Last<T> который получает массив T и возвращает его последний элемент.

type Last<T extends unknown[]> = [unknown, ...T][T['length']];

type arr1 = ['a', 'b', 'c'];
type arr2 = [3, 2, 1];

type tail1 = Last<arr1>; // expected to be 'c'
type tail2 = Last<arr2>; // expected to be 1

Pop

Реализуйте дженерик Pop<T>, который принимает массив T и возвращает массив без последнего элемента.

type Pop<T> = T extends [...infer R, unknown] ? R : T;

type arr1 = ['a', 'b', 'c', 'd'];
type arr2 = [3, 2, 1];

type re1 = Pop<arr1>; // expected to be ['a', 'b', 'c']
type re2 = Pop<arr2>; // expected to be [3, 2]

Promise.all

Добавьте типы к функции PromiseAll, которая принимает массив объектов PromiseLike. Возвращаемое значение должно быть Promise<T>, где T — массив результатов.

declare function PromiseAll<T extends any[]>(
  values: readonly [...T],
): Promise<{ [P in keyof T]: T[P] extends Promise<infer R> | infer R ? R : never }>;

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise<string>((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

// expected to be `Promise<[number, 42, string]>`
const p = PromiseAll([promise1, promise2, promise3] as const);

Type Lookup

Иногда вам может понадобиться найти тип в объединении по его атрибутам.

В этой задаче мы хотели бы получить соответствующий тип, выполнив поиск поля общего типа в объединении Cat | Dog. Другими словами, мы ожидаем получить Dog для LookUp<Dog | Cat, 'dog'> и Cat для LookUp<Dog | Cat, 'cat'> в следующем примере:

type LookUp<U, T> = U extends { type: T } ? U : never;

interface Cat {
  type: 'cat';
  breeds: 'Abyssinian' | 'Shorthair' | 'Curl' | 'Bengal';
}

interface Dog {
  type: 'dog';
  breeds: 'Hound' | 'Brittany' | 'Bulldog' | 'Boxer';
  color: 'brown' | 'white' | 'black';
}

type MyDogType = LookUp<Cat | Dog, 'dog'>; // expected to be `Dog`

Trim Left

Реализуйте дженерик TrimLeft<T>, который уделяет пробелы из начала строки.

type Space = ' ' | '\n' | '\t'; // Пробельные символы
type TrimLeft<T extends string> = T extends `${Space}${infer R}` ? TrimLeft<R> : T;

type trimed = TrimLeft<'  Hello World  '>; // expected to be 'Hello World  '

Trim

Реализуйте дженерик Trim<T>, который удаляет пробелы с обоих концов строки.

type Space = ' ' | '\n' | '\t'; // Пробельные символы
type Trim<T extends string> = T extends `${Space}${infer R}` | `${infer R}${Space}` ? Trim<R> : T;

type trimmed = Trim<'  Hello World  '>; // expected to be 'Hello World'

Capitalize

Реализуйте дженерик Capitalize<T>, который преобразует первую букву строки в верхний регистр, а остальную часть строки оставляет как есть.

// F - это первый символ исходной строки, S - остальная часть строки.
type Capitalize<T extends string> = T extends `${infer F}${infer S}` ? `${Uppercase<F>}${S}` : T;

type capitalized = Capitalize<'hello world'>; // expected to be 'Hello world'

Replace

Реализуйте дженерик Replace<S, From, To>, который заменяет строку From на To один раз в заданной строке S.

type Replace<S extends string, From extends string, To extends string> = From extends ''
  ? S
  : S extends `${infer L}${From}${infer R}`
    ? `${L}${To}${R}`
    : S;

type replaced = Replace<'types are fun!', 'fun', 'awesome'>; // expected to be 'types are awesome!'

ReplaceAll

Реализуйте дженерик ReplaceAll<S, From, To> который заменяет все подстроки From на To в строке S.

type ReplaceAll<S extends string, From extends string, To extends string> = From extends ''
  ? S
  : S extends `${infer L}${From}${infer R}`
    ? `${L}${To}${ReplaceAll<R, From, To>}`
    : S;

type replaced = ReplaceAll<'t y p e s', ' ', ''>; // expected to be 'types'

Append Argument

Для заданного типа функции Fn и любого типа A (любой в этом контексте означает, что мы не ограничиваем тип, и я не имею в виду какой-либо тип 😉) создайте общий тип, который будет принимать Fn в качестве первого аргумента, A в качестве второго и создаст функцию типа G, которая будет такой же, как Fn, но с добавленным аргументом A в качестве последнего.

type AppendArgument<Fn extends Function, A> = Fn extends (...args: infer Args) => infer Res
  ? (...args: [...Args, A]) => Res
  : never;

type Fn = (a: number, b: string) => number;

type Result = AppendArgument<Fn, boolean>;
// expected be (a: number, b: string, x: boolean) => number

Permutation

Реализуйте тип перестановки, который преобразует типы объединений в массив, включающий перестановки объединений.

type Permutation<T, K = T> = [T] extends [never] ? [] : K extends K ? [K, ...Permutation<Exclude<T, K>>] : never;

type perm = Permutation<'A' | 'B' | 'C'>; // ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']

Length of String

Вычислите длину строкового литерала, который ведет себя как String#length.

type LengthOfString<S extends string, T extends string[] = []> = S extends `${infer F}${infer R}`
  ? LengthOfString<R, [...T, F]>
  : T['length'];

type len = LengthOfString<'typescript'>; // 10

Flatten

В этой задаче вам нужно будет написать тип, который принимает массив и генерирует тип сглаживания массива (аналогично методу Array#flat).

type Flatten<S extends unknown[], T extends unknown[] = []> = S extends [infer X, ...infer Y]
  ? X extends unknown[]
    ? Flatten<[...X, ...Y], T>
    : Flatten<[...Y], [...T, X]>
  : T;

type flatten = Flatten<[1, 2, [3, 4], [[[5]]]]>; // [1, 2, 3, 4, 5]

Append to object

Реализуйте тип, который добавляет новое поле в интерфейс. Тип принимает три аргумента. Выходными данными должен быть объект с новым полем.

type AppendToObject<T, U extends PropertyKey, V> = { [K in keyof T | U]: K extends keyof T ? T[K] : V };

type Test = { id: '1' };
type Result = AppendToObject<Test, 'value', 4>; // expected to be { id: '1', value: 4 }

Absolute

Реализуйте тип Absolute. Тип, принимающий строку, число или bigint. Выходные данные должны быть строкой положительных чисел.

type Absolute<T extends number | string | bigint> = `${T}` extends `-${infer U}` ? U : `${T}`;

type Test = -100;
type Result = Absolute<Test>; // expected to be "100"

String to Union

Реализуйте тип StringToUnion<T> принимающий строковый аргумент. Выходные данные должны представлять собой объединение входных букв.

type StringToUnion<T extends string> = T extends `${infer Letter}${infer Rest}` ? Letter | StringToUnion<Rest> : never;

type Test = '123';
type Result = StringToUnion<Test>; // expected to be "1" | "2" | "3"

Merge

Объедините два типа в новый тип. Ключи второго типа переопределяют ключи первого типа.

type Merge<F, S> = { [K in keyof F | keyof S]: K extends keyof S ? S[K] : K extends keyof F ? F[K] : never };

type foo = {
  name: string;
  age: string;
};
type coo = {
  age: number;
  sex: string;
};

type Result = Merge<foo, coo>; // expected to be {name: string, age: number, sex: string}

KebabCase

Замените camelCase или PascalCase строку на kebab-case.

type KebabCase<S> = S extends `${infer S1}${infer S2}`
  ? S2 extends Uncapitalize<S2>
    ? `${Uncapitalize<S1>}${KebabCase<S2>}`
    : `${Uncapitalize<S1>}-${KebabCase<S2>}`
  : S;

type FooBarBaz = KebabCase<'FooBarBaz'>;
const foobarbaz: FooBarBaz = 'foo-bar-baz';

type DoNothing = KebabCase<'do-nothing'>;
const doNothing: DoNothing = 'do-nothing';

Diff

Создайте Object который представляет собой разницу между O и O1

type Diff<O, O1> = {
  [K in keyof (O & O1) as K extends keyof (O | O1) ? never : K]: (O & O1)[K];
};

type Foo = {
  name: string;
  age: string;
};

type Bar = {
  name: string;
  age: string;
  gender: number;
};

type Result = Diff<Foo, Bar>; // { gender: number }

AnyOf

Реализуйте Python функцию any(iterable) в системе типов. Тип принимает массив и возвращает значение true, если какой-либо элемент массива имеет значение true. Если массив пуст, верните false.

type Falsy = 0 | '' | false | [] | undefined | null | { [key: PropertyKey]: never };
type AnyOf<T extends any[]> = T[number] extends Falsy ? false : true;

type Sample1 = AnyOf<[1, '', false, [], {}]>; // expected to be true.
type Sample2 = AnyOf<[0, '', false, [], {}]>; // expected to be false.