Статична валідація в TypeScript. Коли використовувати перезавантаження функції, мапування типів, рекурсії типів чи умовних типів

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Привіт, мене звати Сергій. Працюю 5 років як Front-end програміст, здебільшого з TypeScript. Ця стаття буде присвячена статичній валідації в TypeScript. Я спробую показати, коли краще використовувати перезавантаження функції, мапування типів, рекурсії типів чи умовних типів. Усі приклади взяті з практики.

Виведення точних типів

Розглянемо наступний приклад:

const foo = <T,>(a: T) => a

// const foo: <42>(a: 42) => 42
const result = foo(42) // 42

Як бачимо, T репрезентує тип літералу 42 а не просто number. У випадку примітивів, вивести строгий тип літералу не складно. Складніше, коли маємо об’єкт.

const foo = <T,>(a: T) => a

// const foo: <{ a: number; }> (a: { a: number; }) => { a: number; }
foo({ a: 42 })

Тепер T репрезентує {a: number}, а не {a: 42}. То як вивести {a: 42}? Потрібно додати більше обмежень (constraints) до T і використати ще один generic для значення з ключем a.

const foo = <A extends number, T extends { a: A }>(arg: T) => arg

// const foo: <number, {a: 42}>(arg: { a: 42 }) => {a: 42}
const result = foo({ a: 42 })

Вищевказана техніка є важливою, тому що дозволяє перевіряти правильність введених аргументів. Більш складніші приклади ви можете знайти тут.

Тепер, коли ми знаємо як вивести літерали типів, можемо перейти до наступного розділу.

Фільтр ключів об’єкта і їхня валідація

Уявімо, що маємо функцію, яка очікує два аргумента: об’єкт та ключ, який відповідає значенню типу number. Для прикладу:

const obj = {
  age: 42,
  name: 'John'
}

numericalKey(obj, 'age') // ok
numericalKey(obj, 'name') // error

Передавання ключа age є допустиме, тому що він відповідає значенню 42, яке має тип number, тоді як передавання ключ name до функції є неправильним, оскільки він відповідає значенню з типом string.

Перед тим як приступити до написання функції, необхідно написати тип, який буде фільтрувати необхідні нам ключі.

type NumberKey<Obj> = {
  [Key in keyof Obj]: Obj[Key] extends number ? Key : never
}[keyof Obj]

// Tests
type _ = NumberKey<{ age: 42 }> // "age"
type __ = NumberKey<{ age: 42, name: 'John' }> // "age"
type ___ = NumberKey<{ a: 42, b: 'John', c: -0 }> // "a" | "c"

Щоб краще зрозуміти як цей тип працює, спробуйте усунути [keyof Obj]:

type NumberKey<Obj> = {
  [Key in keyof Obj]: Obj[Key] extends number ? Key : never
}

// type Result = {
//     age: "age";
//     name: never;
// }
type Test = NumberKey<{ age: 42, name: 'John' }> // "age"

Як бачимо, NumberKey ітерує по всім ключам і перевіряє, чи значення з необхідним ключем Obj[Key] розширює тип number. Іншими словами, чи Obj[Key] є субтипом number.

На виході отримуємо об’єкт з ідентичними ключами, проте з різними вартостями. То чому ж нам на кінці потрібно ужити [keyof Obj] ? Це є доволі частим питанням.

Справа в тому, що коли ми передамо "age" | "name" до Result["age" | "name"] — ми отримуємо тільки "age", тому що never не впливає на значення union, оскільки він вважається пустим empty union, або іншими словами — нейтральними елементом . Це так само, як 1 + 0 = 1. Ми можемо усунути операцію додавання 0 і вираз від цього не зміниться.

Тепер, коли ми знаємо, як отримати усі дозволені ключі, можемо приступити до написання функції.

const obj = {
  age: 42,
  name: 'John'
}

type NumberKey<Obj> = {
  [Key in keyof Obj]: Obj[Key] extends number ? Key : never
}[keyof Obj]

const numericalKey = <Obj, Key extends NumberKey<Obj>>(obj: Obj, key: Key) => obj[key]

numericalKey(obj, 'age') // ok
numericalKey(obj, 'name') // error

Як бачимо, усе працює, як треба. Разом з тим, obj[key] в тілі функції numericalKey не вважається значенням з типом number. Тобто ми не можемо викликати obj[key].toFixed(). TypeScript взагалі не дозволить нам викликати будь-який метод, тому що TypeScript не знає нічого про цей тип Obj[Key].

Для того, щоб TS розумів, з чим він має справу, ми мусимо додати певні обмеження до Obj.

const numericalKey = <
    Obj extends Record<string, number | string>, // обмеження
    Key extends NumberKey<Obj>
>(obj: Obj, key: Key) => {
    obj[key].toLocaleString() // ok
    obj[key].toString() // ok
    obj[key].valueOf() // ok
}

Тепер TypeScript дозволить нам викликати тільки ті методи для obj[key], які є спільними для типів string та number. Схожі питання можна знайти на stackoverflow тут і тут.

Видалення ключів з відповідним префіксом

Давайте зробимо задачу дещо складнішою: заборонимо використання ключів, які починаються з нижнього підкреслення.

type NoUnderscore<T> = T extends `_${infer _}` ? never : T extends string ? T : never

type Test = NoUnderscore<'_hello'> // never
type Test2 = NoUnderscore<'hi'> // hi

NoUnderscore — перевіряє чи перший символ в T розширює тип _${string}. Якщо так, значить, наш ключ є заборонений. В іншому випадку ми повинні переконатись, чи T є взагалі string.

type NumberKey<Obj> = {
  [Key in keyof Obj]: Obj[Key] extends number ? Key : never
}[keyof Obj]

type NoUnderscore<T> = T extends `_${infer _}` ? never : T extends string ? T : never

const numericalKey = <
  Obj,
  Key extends NumberKey<Obj>
>(obj: Obj, key: NoUnderscore<Key>) => obj[key]

numericalKey({
  _age: 42,
  name: 'John'
}, 'age') // error

numericalKey({
  count: 42,
  name: 'John'
}, 'count') // ok

numericalKey({
  count: 42,
  name: 'John'
}, 'name') // error

Фільтрація за значенням

Тепер, коли ми знаємо, як здійснювати валідацію ключів, можемо перейти до валідації значень. Додамо ще одну вимогу до нашого коду. Нам дозволено використовувати ключ, якщо його значення ділиться на 5. Доволі дивна вимога, але все ж таки є попит. Цей випадок є дещо складніший, тому що нам потрібно вивести точний (literal) тип аргументу. Щоб це зробити, необхідно використати або оператор as const (immutable assertion), або додати додатковий дженерик (generic).

Усі числа які закінчуються на 5 або нуль — діляться на 5.

Щоб переконатись, чи число ділиться на 5, потрібно перетворити його в string.

type ZeroOrFive = '0' | '5'

type DividedByFive<Obj, Prop extends keyof Obj> =
  Obj[Prop] extends number
  ? `${Obj[Prop]}` extends '5' | `${number}${ZeroOrFive}`
  ? Prop
  : never
  : never

type Test = DividedByFive<{ age: 35 }, 'age'> // 5
type Test2 = DividedByFive<{ age: 352 }, 'age'> // never

Тепер можемо написати функцію:

type ZeroOrFive = '0' | '5'

type DividedByFive<Obj, Prop extends keyof Obj> =
  Obj[Prop] extends number
  ? `${Obj[Prop]}` extends '5' | `${number}${ZeroOrFive}`
  ? Prop
  : never
  : never

const numericalKey = <
  Key extends string,
  Value extends string | number,
  Obj extends Record<Key, Value>,
  >(obj: Obj, key: DividedByFive<Obj, Key>) => obj[key]

numericalKey({
  age: 4,
  name: 'John'
}, 'age') // error

numericalKey({
  age: 45,
  name: 'John'
}, 'age') // ok

Рекурсивні типи

Якщо ви часто використовуєте рекурсію в типах, то варто переконатись, що версія TypeScript є не меншою за 4.5.

Валідація RGB/IP або створення u8

Отже, припустимо, ви хочете написати функцію, яка отримуватиме колір в форматі RGB. Пригадаю, що цей формат складається з трьох октетів (u8), тобто максимальне значення кожної цифри — 255 (0xff).

Давайте почнемо з написання типу до одного октету. Тобто маємо написати тип, який репрезентує unsigned integer. В TS u8 можна репрезентувати через union всіх можливих значень. Тобто 0 | 1 | 2 .. 254 | 255.

type MAXIMUM_ALLOWED_BOUNDARY = 256

type ComputeRange<
    N extends number,
    Result extends Array<unknown> = [],
    > =
    (Result['length'] extends N
        ? Result
        : ComputeRange<N, [...Result, Result['length']]>
    )

type Octal = ComputeRange<MAXIMUM_ALLOWED_BOUNDARY>[number] // 0 - 255

ComputeRange отримує два аргументи. Перший аргумент N — незмінний протягом усієї ітерації, це є власне максимально допустиме число. Другий аргумент, це є таблиця, яка кожної ітерації збільшується в розмірі на один елемент і цей один елемент відповідає довжині таблиці. Тепер можемо написати функцію.

type MAXIMUM_ALLOWED_BOUNDARY = 256

type ComputeRange<
    N extends number,
    Result extends Array<unknown> = [],
    > =
    (Result['length'] extends N
        ? Result
        : ComputeRange<N, [...Result, Result['length']]>
    )

type Octal = ComputeRange<MAXIMUM_ALLOWED_BOUNDARY>[number]

type Digits = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

type AlphaChanel = `0.${Digits}` | '1.0'

type RGBA<Alpha extends number = 1.0> = [Octal, Octal, Octal, (`${Alpha}` extends AlphaChanel ? Alpha : never)?]

function getContrastColor<Alpha extends number>(...[R, G, B, a]: RGBA<Alpha>) {}

getContrastColor(10, 20, 30, 0.2); // ok
getContrastColor(256, 20, 30, 0.2); // error, 256 is out of the range
getContrastColor(255, 20, 30, 0.22); // error, 0.22 should be 0.2

Я додав ще AlphaChanel, який приймає значення від 0.1 до 0.998.

Тут можна знайти відповідне питання на stackoverflow. Тут можна знайти мою відповідь щодо створення діапазону чисел в TypeScript.

Валідація повторюваності

Цей патерн можна широко застосовувати, якщо маєму повторюваність. Наприклад, маємо string, який складається з послідовності ${number}, ${number};. Тобто значення 45,65; 78,12; 98,34; є дозволене, а 23,45,56;11,11; — ні. Алгоритм такий: нам потрібно створити максимально довгий union, де кожний наступний елемент буде злиття попереднього з ${number}, ${number};.

Щось на кшталт такого:

type Coordinates = `${number},${number};`;

type Result =
    | `${number},${number};`
    | `${number},${number};${number},${number};`
    | `${number},${number};${number},${number};${number},${number};`

Тільки ми повинні динамічно створити такий тип.

type MAXIMUM_ALLOWED_BOUNDARY = 10

type Coordinates = `${number},${number};`;

type Last<T extends string[]> =
    T extends [...infer _, infer Last]
    ? Last
    : never;

type ConcatPrevious<T extends any[]> =
    Last<T> extends string
    ? `${Last<T>}${Coordinates}`
    : never

type Repeat<
    N extends number,
    Result extends Array<unknown> = [Coordinates],
    > =
    (Result['length'] extends N
        ? Result
        : Repeat<N, [...Result, ConcatPrevious<Result>]>
    )

type MyLocation = Repeat<MAXIMUM_ALLOWED_BOUNDARY>[number]

const myLocation1: MyLocation = '02,56;67,68;' // ok
const myLocation2: MyLocation = '45,56;67,68;1,2;3,4;5,6;7,8;9,10;' // ok
const myLocation3: MyLocation = '45,56;67,68;1,2;3,4;5,6;7,8;9,10,' // expected error no semicolon at the end

Last — виводить останній елемент з таблиці.

Repeat — працює подібно до попереднього прикладу, тільки замість додавання в кінець довжини таблиці — ми додаємо Coordinates до останнього елементу. Я розумію, що часом такі типи важко відразу зрозуміти, тому тримайте runtime відповідник :

/**
 * JS representation of Repeat type
 */
const mapped = (N: number, Result: any[] = []): string => {
    if (N === Result.length) {
        return Result.join('')
    }

    const x = Math.random();
    const y = Math.random()
    return mapped(N, [...Result, `${x}${y}`])
}

Тут ви зможете знайти відповідь.

Валідація рівня вкладеності

Припустимо, ми хочемо створити об’єкт з рекурсивною властивістю children. Наприклад, такий:

const result = {
    level: 0,
    children: {
        level: 1,
        children: {
            level: 2,
            children: {
                level: 3,
                children: {
                    level: 4,
                    children: undefined
                }
            }
        }
    }
}

У цьому прикладі не йдеться про створення просто рекурсивного типу:

type Layer = {
    level: number;
    children: Layer | undefined
}

Йдеться про створення типу, в якому можемо заздалегідь вказати кількість вкладеностей. Можна вказати менше варств, але не більше. Всі пояснення знайдете в коментарях до типу.

type Test<T, Length extends number, Tuple extends number[] = []> =
    /**
     * Якщо Length відповідає довжині Tuple
     * - повертаємо перший аргумент T
     */
    (Length extends Tuple['length']
        ? T
        /**
         * В іншому разі ітеруємо через всі ключі + "level"
         */
        : {
            [Prop in keyof T | 'level']:
            /**
             * Якщо поточний ключ це "level"
             */
            (Prop extends 'level'
                /**
                 * повертаємо довжину Tuple, таки чином отримуємо { length: 0, .... length: 2}
                 */
                ? Tuple['length']
                /**
                 * Якщо поточний елемент це "children"
                 */
                : (Prop extends 'children'
                    /**
                     * Перевіряємо чи поточна довжина Tuple + 1 є рівна Length
                     */
                    ? (Length extends [...Tuple, 1]['length']
                        /**
                         * якщо так - викликаємо Test з undefined
                         */
                        ? undefined | Test<undefined, Length, [...Tuple, 1]>
                        /**
                         * в іншому разі (якщо не досягнутий максимум)
                         * ітеруєми далі при цьому збільшуючи довжину Tuple на 1
                         */
                        : undefined | Test<T, Length, [...Tuple, 1]>)
                    : never)
            )
        })

const result: Test<{ children: 0 }, 2> = {
    level: 0,
    children: {
        level: 1,
        children: undefined
    }
}

Tuple — відіграє роль індекса. Довжина Tuple дорівнює рівню вкладеності. Тут можна знайти відповідь.

Перезавантаження функцій (function overloads)

Розглянемо наступний приклад.

const conditional = <T,>(arg: T): T extends number ? string : T =>
    typeof arg === 'number' ? arg.toString() : arg

Дуже часто на StackOverFlow люди питають, чому цей код не працює та як змусити його працювати. TypeScript не підтримує умовні типи в місці, де очікується тип повернення. Натомість, ми можемо перезавантажити функцію.

function conditional<T,>(arg: T): T extends number ? string : T
function conditional<T,>(arg: T) {
    return typeof arg === 'number' ? arg.toString() : arg
}
conditional(1) // string
conditional('str') // 'str'

Чому перезавантаження працює ? Тому що воно є біваріантним щодо функції. Це значить, що тип повернення в перезавантаженій функції може бути присвоєний до типу повернення основної функції або навпаки — typescript дозволить такий тип.

Погодьтеся, умовні типи часом важко читаються, тому в цьому випадку ми можемо написати функцію по-іншому:

function conditional<T extends number>(arg: T): string
function conditional<T extends string>(arg: T): T
function conditional<T,>(arg: T) {
    return typeof arg === 'number' ? arg.toString() : arg
}
conditional(1) // string
conditional('str') // 'str'

Перезавантаження функцій з таблицями

Дуже часто перезавантаження стають в пригоді, коли працюємо з таблицями. Розглянемо наступний приклад:

type Stringify<Tuple extends number[]> = {
    [Prop in keyof Tuple]: `${Tuple[Prop] & number}`
}

type Test = Stringify<[1, 2, 3]> // ["1", "2", "3"]

const handler = <Tuple extends number[]>(tuple: [...Tuple]): Stringify<Tuple> => {
    return tuple.map(elem => elem.toStirng()) // error
}

// ["1", "2", "3"]
const result = handler([1, 2, 3])

Функція handler повертає нам нову таблицю, де кожний має тип string. Як ви вже здогадались, щоб виправити помилку, потрібно перезавантажити функцію.

function handler<Tuple extends number[]>(tuple: [...Tuple]): Stringify<Tuple>
function handler(tuple: number[]) {
    return tuple.map(elem => elem.toString()) // ok
}

// ["1", "2", "3"]
const result = handler([1, 2, 3])

Stringify — ітерує через всі елементи таблиці і замінює їх на нову версію з типом string.

Виділення недозволених елементів

Припустимо, наша функція не дозволяє аргументів, де є нулі. Крім того, якщо в таблиці є нуль, ми хочемо, щоб тільки він був підкреслений, а не цілий аргумент. Разом з тим, тип повернення має бути без нулів взагалі.

// Замінимо кожний 0 на never
type ZeroValidation<Tuple extends number[]> = {
    [Prop in keyof Tuple]: Tuple[Prop] extends 0 ? never : Tuple[Prop]
}

type FilterZero<Tuple extends any[], Result extends any[] = []> =
    /**
     * Якщо перший аргумент це пуста таблиця
     * повертаємо Result
     */
    (Tuple extends []
        ? Result
        /**
         * В іншому випадку виводимо перший елемент з таблиці і всі інщі елементи
         */
        : (Tuple extends [infer Head, ...infer Rest]
            /**
             * Якщо перший елемент це 0
             */
            ? (Head extends 0
                /**
                 * Викликаємо рекурсивно FilterZero і губимо 0
                 */
                ? FilterZero<Rest, Result>
                /**
                 *  Викликаємо рекурсивно FilterZero і додаємо поточний елемент до Result
                 */
                : FilterZero<Rest, [...Result, Head]>)
            : never)
    )

function handler<Tuple extends number[]>(tuple: ZeroValidation<[...Tuple]>): FilterZero<Tuple>
function handler<Tuple extends number[]>(tuple: ZeroValidation<[...Tuple]>) {
    return tuple.filter(elem => elem ! == 0)) // ok
}

// [2, 3]
const result = handler([0, 2, 3]) // 0 підкреслений як недозводлений елемент

Перезавантаження React-компонентів

У цьому розділі під поняттям React-компонент я розумію функційний компонент з типом React.FC

Дуже часто ми забуваємо, що React-компоненти — це є ті ж самі функції, які також можна перезавантажити. Візьмімо тривіальний приклад. Маємо один компонент, який може приймати властивості або Checkbox, або Dropdown.

import React, { FC } from 'react'

type Checkbox = {
    checked: boolean;
};

type Dropdown = {
    options: Array<any>;
    selectedOption: number;
};

const Component: FC<Dropdown | Checkbox> = (props) => <></>

const CheckboxComponent = <Component checked={true} /> // ok

const DropdownComponent = <Component options={[]} selectedOption={0} /> // ok

const MixComponent = <Component checked options={[]} selectedOption={0} /> // ok, but should be error

Playground

Як бачимо, union типи в TypeScript працюють не так, як ми очікуємо. Ми не маємо помилки в останньому прикладі, тому що TypeScript має структуральну систему типів. Для того, щоб викликати помилку в останньому прикладі, ми можемо додати властивість type, яка буде відігравати роль дискримінанта.

type Checkbox = {
    type: 'Checkbox';
    checked: boolean;
};

type Dropdown = {
    type: 'Dropdown'
    options: Array<any>;
    selectedOption: number;
};

Ця техніка спрацює, тільки в цьому випадку властивість type нам взагалі не потрібна в runtime. Ми можемо просто перезавантажити функцію. Візьміть до уваги, що крім стандартного синтаксису, перезавантаження можна створити шляхом злиття двох функцій.

import React, { FC } from 'react'

type Checkbox = {
    checked: boolean;
};

type Dropdown = {
    options: Array<any>;
    selectedOption: number;
};

const Component: FC<Dropdown> & FC<Checkbox> = (props) => <></>
const MixComponent = <Component checked options={[]} selectedOption={0} /> // expected error

Перезавантаження — FC<Dropdown> & FC<Checkbox>. Давайте зробимо завдання дещо складнішим. Заборонимо використання 0 як значення для selectedOption.

type Checkbox = {
    checked: boolean;
};

type Dropdown = {
    options: Array<any>;
    selectedOption: number;
};

type Validation<N extends number> = N extends 0 ? never : N

function Component(props: Checkbox): null
function Component<Option extends number>(props: Dropdown & { selectedOption: Validation<Option> }): null
function Component(props: Checkbox | Dropdown) {
    return null
}

const NonZero = <Component options={[]} selectedOption={1} /> // ok
const WithZero = <Component options={[]} selectedOption={0} /> // expected error

Як бачимо, ми змушені переключитись на стандартний синтаксис перезавантажень, тому що нам необхідно вивести тип selectedOption властивості. Подібний приклад ви можете знайти на stackoverflow та в моїй статті на dev.to.

Підсумок

У цій статті я хотів показати проблеми, з якими часто стикаються інші розробники. Я не говорю, що вам необхідно вже і зараз переходити на TypeScript. Хотілося б уникнути будь-яких суперечок. Разом з тим, існує декілька цікавих альтернатив, наприклад ReScript чи Fable.

  1. З мого досвіду, TypeScript найкраще себе показує, коли працюємо з незмінними (immutable) структурами даних.
  2. Умовні типи з багатьма рівнями вкладеності так само важко читаються, як і вкладені if/else блоки, тому їх варто розбивати на невеликі допоміжні типи.
  3. Перед тим як писати перезавантаження функції, подумайте, чи не варто просто переказати union.
  4. Варто писати невеликі type-тести, якщо маєте багато допоміжних типів.

Сподобалась стаття? Натискай «Подобається» внизу. Це допоможе автору виграти подарунок у програмі #ПишуНаDOU

👍ПодобаєтьсяСподобалось17
До обраногоВ обраному12
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Забавные костыли для подпирания костылей ЖС)

Может кто объяснить почему в блоке

Виділення недозволених елементів

при объявлении сигнатуры

function handler(tuple: ZeroValidation<[...Tuple]>): FilterZero

параметр tuple имеет тип именно

ZeroValidation<[...Tuple]>

а не просто?

ZeroValidation<-Tuple> (минус перед Tuple, потому что разметка не даёт вставить по-другому)

Ведь по сути тип Tuple это уже является

extends number[]

зачем разворачивать массив из Tuple и опять заворачивать Tuple в массив? я не пойму

И да, я вижу что в моём предложенном варианте с

ZeroValidation<-Tuple> (минус перед Tuple, потому что разметка не даёт вставить по-другому)

при вызове

handler([0, 2, 3])

перестаёт ругаться на ноль как на невалидный элемент — интересно понять, что происходит под капотом и почему это так

ZeroValidation<[...Tuple]>

а не

ZeroValidation

тому що потрібно вивести кожний елемент. Подивіться бульласка www.typescriptlang.org/...​html#variadic-tuple-types і мою статтю catchts.com/infer-arguments

Замість «мінуса», можете використовувати «кому» після Tuple

по вашим ссылкам ответа на почему нужно использовать именно

ZeroValidation<[...Tuple]>

, а не просто

ZeroValidation<-Tuple>

я не нашел
мне частично помогли понять разницу вот эти комментарии к ответу на стековерфлоу и данный плейграунд

В блоке про

Перезавантаження функцій з таблицями

как я понял вызов в конце

const result = handler([1, 2, 3])

не использует тип Test, объявленный выше как

type Test = Stringify<[1, 2, 3]> // [«1», «2», «3»]

так как по сути мы при вызое handler() можем туда передать массив из любых чисел, а не только, [1, 2, 3], получается нам в данном случае приходится делать костыли вокруг тайпскрипта, т.к. не получается описать конверсию в строке

return tuple.map(elem => elem.toString()) // ok

из string[] в [«1», «2», «3»]

Тип Test немає жодного відношення до виклику функції. Я його використав тільки щоб було зрозуміло як працює Stringify. Можливо я не правильно зрозумів питання. Чи не могли б Ви опублікувати кусок коду в Typescript playground ?

Спасибо за пояснение насчёт типа Test, этот момент меня лично запутал.
Мой вопрос скорее в том, в чем вообще смысл перегрузке в данном примере? Если мы фунцию можем реализовать например в таком виде:

function handler<Tuple extends number[]>(tuple: [...Tuple]): Stringify<Tuple> {
    return tuple.map(elem => elem.toString()) as Stringify<Tuple>;
}
Как я понял в вашем примере, вы по сути «обманываете» тайпскрипт перегружая фунцию — зачем тут перегрузка я не пойму

PS: вот ссылка на плейграунд

КОнкретно в цьому випадку немає перезагрузки функції. Разом з тим, декларування типу який повертається з функції є лишнім. Можна залишити тільки «as Stringify ...»

якщо у Вас є більше питань, то ми можемо здзвонитись і я Вам все детально поясню по кожному прикладу

Я так понимаю в блоке

Фільтрація за значенням

очепятка где

`${Obj[Prop]}` extends ’5′ |

я думаю вы хотели использовать ZeroOrFive вместо ’5′, то есть

`${Obj[Prop]}` extends ZeroOrFive |

Відверто кажучи, я вже не памятаю. Можливо і так. Спробуйте чи Ваш варіант працює. Дякую

Дуже цікаво, дякую за статтю!

А потом почти весь этот замудрённый код превращается в пшик на этапе компиляции в js

Або не перетворюється, якщо є помилки компіляції TS.. Для цього TS взагалі і існує

Typescript це виключно статична валідація

Підписатись на коментарі