Система типов TypeScript как язык программирования

Тараненко Андрей, Холдинг Т1

Система типов TypeScript как язык программирования

Тараненко Андрей
Холдинг Т1

Андрей Тараненко

  • Представляю холдинг T1
  • 5 лет во frontend’е
  • Все это время пишу на TypeScript
  • Люблю типизировать

Дисклеймер

Будет много кода

Много кода

Не нужно бояться

Оно боится

Самое важное будет объяснено

Главное, чтобы было интересно

Очень интересно

TypeScript ~ Система типов

Причина доклада

Причина тряски?

Template literal types

🙈🙉🐵

Простая типизация повсюду

Оно повсюду

Пример

Capitalize

    const capitalize = (str: string): string => {
      return str.charAt(0).toUpperCase() + str.slice(1);
    };
      
Винни пух

Capitalize

    const capitalize = <T extends string>(str: T) => {
      return (str.charAt(0).toUpperCase() + str.slice(1)) as Capitalize<T>;
    };
      
Крутой Винни пух

А почему так?

TypeScript — Тьюринг-полный

GitHub

Учитывая синтаксис — функциональный ЯП

Язык программирования в языке программирования

А воспринимаете ли вы его так?

Будем программировать на типах

А оно мне надо?

Зачем?

Программисту нужно ТЗ

Цель: типизация для модуля интерполяции

Интерполяция

Пазл

Интерполяция

    const hello = 'hello';
    const world = 'world';
    const helloWorld = `${hello} ${world}!`;
      

Интерполяция

    const template = 'some string with [0] and [1]';

    const text = interpolate(template, ['value1', 'value2']);
      

Интерполяция

    const template = 'some string with [first] and [second]';

    const text = interpolate(template, { first: 'value1', second: 'value2' });
      

✅ — Умеем

❌ — Пока не умеем

Требования

Требования

Итоговый результат

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

      type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (
        k: infer I
      ) => void
        ? I
        : never;

      type ConcatPath<Prefix extends string, Path extends string> = Prefix extends ""
        ? Path
        : `${Prefix}.${Path}`;

      type AnyResource = { [x: string]: string | AnyResource };
    
      type FlattenResource<Resource extends AnyResource, Path extends string> = UnionToIntersection<
        keyof Resource extends infer Keys extends keyof Resource
          ? Keys extends string
        ? Resource[Keys] extends AnyResource
          ? FlattenResource<Resource[Keys], ConcatPath<Path, Keys>>
          : { [x in ConcatPath<Path, Keys>]: Resource[Keys] }
        : never
          : never
      >;

      type InterpolationParams<ResourceString extends string> =
        GetInterpolatingVariablesNames<ResourceString> extends infer U extends string
          ? [U] extends [never]
        ? [param?: {}]
        : [param: Record<U, string>]
          : [param?: {}];

      interface InterpolationMethod<FlatResource extends Record<string, string>> {
        <Key extends keyof FlatResource | (string & {})>(
          key: Key,
          ...[params]: InterpolationParams<FlatResource[Key]>
        ): Key extends keyof FlatResource ? string : null;
      }
    
Боль

Понятия из языков программирования в TS

Переменные

Переменные в JS

    const helloWorld = 'Hello world !';
      

Переменные в TS

    type HelloWorld = 'Hello world !';
      

НО

Переменные в TS

    type HelloWorld = 'Hello' | ' ' | 'world' | '!';
      

Функции

Функции в JS

    function myFunc(arg) {
      return /* */;
    }
      

Функции в TS — это Generics

Generic  — шаблон для создания типов

Базовый синтаксис

    type Generic<T> = {
      payload: T;
    };

    type StringPayload = Generic<string>;
      

Ограничение значений параметров

    type Generic<T extends string> = /* */;
      
Диаграмма супертипов

Введенные понятия

Поиск имен интерполируемых переменных

Боль

Переменные в строках обозначаются {{ name }}

Решение на JS

Решение на JS

    const getInterpolatingVariablesNames = (template) => {
      const regExp = /{{ (?<name>.+) }}/g;

      return template.matchAll(regExp);
    }
      

В TS нет регулярок 😢

Что алгоритмически делает JS под капотом? 🤔

Что алгоритмически делает JS под капотом?

  1. Поиск совпадения с шаблоном

Что нужно, чтобы повторить в TS

Что нужно, чтобы повторить в TS

Возможность многократного исполнения кода

      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
      doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();  doSomething();
    

Циклы

В TS нет циклов

В ассемблере тоже нет циклов

Рекурсия — наши циклы

Рекурсия — наши циклы

    type Generic<T> = Generic<T>
      

Но у нас бесконечная рекурсия

Рекурсия

Нужно условие выхода из рекурсии

Exit

Нужно ветвление

Ветка

Условные типы

Синтаксис

Синтаксис

    type IsString<T> = T extends string
      ? true
      : false;
      

Рекурсия + Условия ≈ Цикл while

Рекурсия + Условия ≈ Цикл while

    type Recursion<T> = T extends Condition
      ? Recursion<ChangeParam<T>>
      : never;
      

Введенные понятия

Что нужно, чтобы повторить в TS

Сопоставление с шаблоном

Сопоставление

Template Literal Types

Template Literal Types

    type Name = 'world';
    type Greet = `Hello ${Name}!`;

    type Greet = 'Hello world!';
      

Template Literal Types

    type Name = 'world' | 'Everyone';
    type Greet = `Hello ${Name}!`;

    type Greet = 'Hello world!' | 'Hello Everyone!';
      

Template Literal Types

    type Greet = `Hello ${string}!`;
      

`${string}` аналогичен (.*)

Можно использовать как супертип с extends

Можно использовать как супертип с extends

    type HasValue<T> = T extends `{{ ${string} }}`
      ? true
      : false

    type Checked = HasValue<'Lorem, {{ name }} dolor'>; // false
        

Важное отличие от регулярок

Шаблону сопоставляется сразу вся строка

Шаблону сопоставляется сразу вся строка

    type HasValue = 'Lorem, {{ name }} dolor' extends `${string}{{ ${string} }}${string}`
      ? true
      : false

    type HasValue = falsetype HasValue = true
      

Что нужно, чтобы повторить в TS

Вычленение имен

Условные типы + infer

Что это такое и как работает?

Как работает infer?

    type ArrayItem<T> = /* */;
      
    type ArrayItem<T> = T extends any[]
      ? /* */
      : never;
      
    type ArrayItem<T> = T extends (infer U)[]
      ? U
      : never;

    type Checked = ArrayItem<number[]>; // number
      

Можно ли что-то подставить, чтобы условие было верным

Вместе с extends infer можно использовать extends

infer extends

    type ArrayItem<T> = T extends (infer U extends string)[]
      ? U
      : never;

    type Checked = ArrayItem<number[]>; // never
      

В одном условии можно использовать несколько infer

Несколько infer

    type TupleItems<T extends [any, any]> = T extends [infer First, infer Second]
      ? First | Second
      : never;

    type Checked = TupleItems<[number, string]>; // number | string
      

Можно использовать для создания переменных

Создание переменных

    type Generic<T> = GetNewType<T> extends infer U
      ? U
      : never;
      

Работает вместе с Template Literal Types

Работает вместе с Template Literal Types

    type Greet = 'Hello world!' extends `Hello ${infer U}!`
      ? U
      : never;

    type Greet = 'world'
      

Что нужно, чтобы повторить в TS

Вспомним алгоритм

  1. Поиск совпадения с шаблоном

Реализация

Реализация

    type GetInterpolatingVariablesNames<T extends string> = /* */
      

Реализация

    type GetInterpolatingVariablesNames<T extends string> =
      T extends /* */
        ? /* */
        : never;
      

Реализация

    type GetInterpolatingVariablesNames<T extends string> =
      T extends `{{ ${string} }}`
        ? /* */
        : never;
      

Реализация

    type GetInterpolatingVariablesNames<T extends string> =
      T extends `{{ ${infer Name} }}`
        ? Name
        : never;
      

Реализация

    type GetInterpolatingVariablesNames<T extends string> =
      T extends `${string}{{ ${infer Name} }}${string}`
        ? Name
        : never;
      

Реализация

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

Слайд №

Требования

Использование вложенных структур и путей к ним в качестве ключей

ЖД Пути

Что имеется в виду?

Что имеется в виду?

    {
      page: {
        card: {
          title: 'Title'
        }
      }
    }
    
interpolate('page.card.title')

Супертип для ресурсного объекта

Супертип для ресурсного объекта

    type AnyResource = {
      [key: string]: string | AnyResource;
    };
      

Алгоритм на JS

Алгоритм на JS

  1. Перебираем ключи по очереди

Алгоритм на JS

    const concatPath = (prefix, path) => prefix ? `${prefix}.${path}` : path;
    const flattenResource = (resource, path) => {
      return Object.keys(resource)
        .map((key) => {
          if (typeof resource[key] === 'object') {
        return flattenResource(resource[key], concatPath(path, key));
          }
          return { [concatPath(path, key)]: resource[key] };
        })
        .reduce((acc, obj) => ({ ...acc, ...obj }), {});
    };
      

Что для этого нужно?

Что для этого нужно?

Получение всех ключей объекта

keyof

keyof

    type HelloWorld = {
      hello: "hello";
      world: "world";
    };
    type Keys = keyof HelloWorld;

    type Keys = 'hello' | 'world';
      

Что для этого нужно?

Как реализовать map

Distributive Conditional Types

Без Distributive Conditional Types

    type HelloWorld = 'Hello' | 'World';
    type Arr = HelloWorld[];
    type Arr = ('Hello' | 'World')[];

Distributive Conditional Types

    type Arr = 'Hello'[] | 'World'[];
      

Как ее запустить?

1️⃣ Использовать условный тип

2️⃣.1️⃣ Проверять аргумент Generic-типа

2️⃣.1️⃣ Проверять аргумент Generic

    type Distributive<T> = T extends any ? T[] : never;

    type ArraysUnion = Distributive<string | number>
    type ArraysUnion = Distributive<string> | Distributive<number>
    type ArraysUnion = string[] | number[]
  
type ArraysUnion = Distributive<never>
Загадка от Жака Фреско
type ArraysUnion = never

2️⃣.2️⃣ Проверять выведенный тип

2️⃣.2️⃣ Проверять выведенный тип

    type Distributive = 'Hello' | 'World' extends infer U
      ? U extends any
        ? U[]
        : never
      : never;
      

Что для этого нужно?

Создание объекта с созданным ключом

Mapped Types

Mapped Types

    type Keys = 'hello' | 'world';

    type Mapped = {
      [Key in Keys]: Transform<Key>
    }
      

Что для этого нужно?

Reduce в один объект

Union To Intersection

{} | {} | {}

{} & {} & {}

Нет механизма, есть хак

Особенность union’а функций

Особенность union’а функций

    type FunctionsUnion =
      | ((arg: { hello: true }) => void)
      | ((arg: { world: true }) => void);
      

Особенность union’а функций

    type FunctionArgument = FunctionsUnion extends (arg: infer U) => void
      ? U
      : never;
      

Особенность union’а функций

    type FunctionArgument = OverloadedFunction extends (arg: infer U) => void
      ? U
      : never;

    type FunctionArgument = {
      hello: true;
    } & {
      world: true;
    };
      

Union To Intersection

Union To Intersection

    type UnionToIntersection<U> = (
      U extends unknown ? (k: U) => void : never
    ) extends (k: infer I) => void
      ? I
      : never;
      

Union To Intersection

    type First = { x: number };
    type Second = { y: number };
    type Point = UnionToIntersection<First | Second>;

    type Point = { x: number; y: number };
      
Сделай нас едиными

Union To Intersection

    type Check = UnionToIntersection<string | number>;
      

Что для этого нужно?

Еще раз алгоритм

  1. Перебираем ключи по очереди
  2. Склеиваем ключ и путь до текущего уровня
  3. Если внутри ключа строка, кладем по пути строку
  4. Иначе кладем результат выполнения функции для уровня ниже

Реализация на TS

Реализация на TS

    type ConcatPath<Prefix extends string, Path extends string> = /*  */;
      

Реализация на TS

    type ConcatPath<Prefix extends string, Path extends string> = /*  */;
    type FlattenResource<
      Resource extends AnyResource,
      Path extends string
    > = /*  */
      

Реализация на TS

    type ConcatPath<Prefix extends string, Path extends string> = /*  */;
    type FlattenResource<
      Resource extends AnyResource,
      Path extends string
    > = keyof Resource extends infer Keys extends keyof Resource
      ? Keys extends string
        ? /*  */


        : never
      : never;
      

Реализация на TS

    type ConcatPath<Prefix extends string, Path extends string> = /*  */;
    type FlattenResource<
      Resource extends AnyResource,
      Path extends string
    > = keyof Resource extends infer Keys extends keyof Resource
      ? Keys extends string
        ? Resource[Keys] extends AnyResource
          ? /*  */
          : /*  */
        : never
      : never;
      

Реализация на TS

    type ConcatPath<Prefix extends string, Path extends string> = /*  */;
    type FlattenResource<
      Resource extends AnyResource,
      Path extends string
    > = keyof Resource extends infer Keys extends keyof Resource
      ? Keys extends string
        ? Resource[Keys] extends AnyResource
          ? FlattenResource<Resource[Keys], ConcatPath<Path, Keys>>
          : /*  */
        : never
      : never;
      

Реализация на TS

    type ConcatPath<Prefix extends string, Path extends string> = /*  */;
    type FlattenResource<
      Resource extends AnyResource,
      Path extends string
    > = keyof Resource extends infer Keys extends keyof Resource
      ? Keys extends string
        ? Resource[Keys] extends AnyResource
          ? FlattenResource<Resource[Keys], ConcatPath<Path, Keys>>
          : { [x in ConcatPath<Path, Keys>]: Resource[Keys] }
        : never
      : never;
      

Реализация на TS

    type ConcatPath<Prefix extends string, Path extends string> = /*  */;
    type FlattenResource<
      Resource extends AnyResource,
      Path extends string
    > = UnionToIntersection<
      keyof Resource extends infer Keys extends keyof Resource
        ? Keys extends string
          ? Resource[Keys] extends AnyResource
        ? FlattenResource<Resource[Keys], ConcatPath<Path, Keys>>
        : { [x in ConcatPath<Path, Keys>]: Resource[Keys] }
          : never
        : never
    >;
      

Требования

Метод интерполяции

Метод интерполяции

    type InterpolationParams<ResourceString extends string> = {/* */};
      

Метод интерполяции

    type InterpolationParams<ResourceString extends string> = {/* */};

    interface InterpolationMethod<FlatResource extends Record<string, string>> {
      <Key extends string>(
        key: Key,
        params: InterpolationParams<FlatResource[Key]>
      ): Key extends keyof FlatResource ? string : null;
    }
      

params — обязательный параметр

Нам помогут ...rest и кортежи

...rest

  const func = (...args: any[]) => /* */;
      

Кортеж

  type Optional = [number?];
  type NamedOptional = [param?: number];
      

...rest + кортеж + условный тип

Тип параметров

  type InterpolationParams<
    ResourceString extends string,
  > = GetInterpolatingVariablesNames<ResourceString> extends infer U
    ? [U] extends [never] // Важно из-за дистрибутивности
      ? [param?: {}]
      : [param: Record<U, string>]
    : never;
      

Метод интерполяции

  interface InterpolationMethod<FlatResource extends Record<string, string>> {
    <Key extends string>( // Нет подсказок
      key: Key,
      ...[params]: InterpolationParams<FlatResource[Key]>
    ): Key extends keyof FlatResource ? string : null;
  }
      
Нет подсказок?

Метод интерполяции

  interface InterpolationMethod<FlatResource extends Record<string, string>> {
    <Key extends keyof FlatResource>(
      key: Key,
      ...[params]: InterpolationParams<FlatResource[Key]>
    ): Key extends keyof FlatResource ? string : null;
  }
      

Объединение со строкой

  type OrAnyString = 'hello' | 'world' | string

  type OrAnyString = string 😢😢😢
        

Объединение со строкой

  type OrAnyString = 'hello' | 'world' | (string | {})

Метод интерполяции

  interface InterpolationMethod<FlatResource extends Record<string, string>> {
    <Key extends keyof FlatResource | (string & {})>(
      key: Key,
      ...[params]: InterpolationParams<FlatResource[Key]>
    ): Key extends keyof FlatResource ? string : null;
  }
      

На практике

    import { AnyResource, FlattenResource, InterpolationMethod } from "./types";

    class Client<Resource extends AnyResource> {
      constructor(private resource: Resource) {}

      interpolate!: InterpolationMethod<FlattenResource<Resource, "">>;
    }
        

А как практиковаться?

А как практиковаться?

Type Challenges / Type Hero

Type Challenges Type Hero

Ссылка на слайды

QR-код на слайды Слайды

Следующий слайд яркий

Вспышка

Было сложно, но мы справились

Тараненко Андрей
Холдинг Т1

QR