Вывод типа для значения параметра автозаполнения перегруженного метода

Я пытаюсь добавить поддержку автозаполнения для параметра универсального метода.

 subscribeToTelemetry<T extends keyof TypeMap>(name: T): Stream<TypeMap[T]>;
 subscribeToTelemetry<T extends NonNullable<{}>>(name: EntityName<T>): Stream<T>; 

Короче говоря, если тип не указан, тип возвращаемого значения должен быть выведен на основе имени и некоторых доступных внедренных/автогенерируемых типов. Кроме того, возможные предложения имен должны быть сделаны с использованием доступных внедренных/автогенерированных типов. Например, используя следующий тип:

type TypeMap = {
    ranger1: RangerTelemetry,
    ranger2: RangerTelemetry,
    mavic2: DroneTelemetry
};

и следующий код

// should suggest: ['mavic2' | 'ranger2' | 'ranger1'] since no type T is provided 
const myStream1 = r.subscribeToTelemetry('ranger1'); // myStream1: Stream<RangerTelemetry

Рекомендуемое значение должно быть ['mavic2' | 'рейнджер2' | 'ranger1'], поскольку тип T не указан, а тип myStream1 будет subscribeToTelemetry

Напротив, если мы предоставим тип Stream<RangerTelemetry> и учитывая доступный внедренный/автогенерированный тип

const entityNames = {
    RangerTelemetry: ['ranger1', 'ranger2', 'ranger3'],
    DroneTelemetry: ['mavic1', 'mavic2', 'mavic3'],
} as const;

и некоторые другие трюки типа (см. ссылку на игровую площадку), используя следующий код

// should suggest only: ['mavic1' | 'mavic2' | 'mavic3'] since type T is provided as DroneTelemetry,
const myStream3 = r.subscribeToTelemetry<DroneTelemetry>('mavic1');

Мы должны предложить: ['mavic1' | 'мавик2' | 'mavic3'], так как был указан тип subscribeToTelemetry, но ['mavic1' | 'мавик2' | Вместо этого отображаются «mavic3», «ranger1», «ranger2», «ranger3»]. Тип DroneTelemetry будет myStream3

У меня есть несколько вариантов реализации подписи перегруженного метода Stream<DroneTelemetry>, но мне не удается предоставить правильные предложения при предоставлении типа. Я всегда получаю предложение рейнджеров, даже когда указываю subscribeToTelemetry в качестве типа.

class Registry {
    subscribeToTelemetry<T extends keyof TypeMap>(name: T): Stream<TypeMap[T]>;
    subscribeToTelemetry<T extends NonNullable<{}>>(name: EntityName<T>): Stream<T>;
    subscribeToTelemetry<T extends NonNullable<{}>>(name: EntityName<T>): Stream<T> {
        return new Stream<T>(name); 
    }
}

Если у кого-нибудь есть предложения, буду признателен за любую помощь

Умберто

Вот полный код:

type DroneTelemetry = { kind: 'DroneTelemetry', batteryLevel: number, velocity: number, location: {latitude: number, longitude: number}}
type RangerTelemetry = { kind: 'RangerTelemetry', velocity: number, location: {latitude: number, longitude: number}}


////// the entity names and type mapping will be autogenerated
const entityNames = {
    RangerTelemetry: ['ranger1', 'ranger2', 'ranger3'],
    DroneTelemetry: ['mavic1', 'mavic2', 'mavic3'],
} as const;

type TypeMap = {
    ranger1: RangerTelemetry,
    ranger2: RangerTelemetry,
    mavic2: DroneTelemetry
};
////////

// Define the types for the keys and values of the mapping
type EntityNames = typeof entityNames;
type EntityKeys = keyof EntityNames;
type EntityValues<T extends EntityKeys> = EntityNames[T][number];

// Define a mapping from type aliases to their corresponding keys
type TypeToKey<T> = T extends { kind: infer K } ? K : never;

type ExtractEntitiesNames<T> = TypeToKey<T> extends keyof EntityNames ? EntityValues<TypeToKey<T>> : never;
type EntityName<T> = ExtractEntitiesNames<T> |  (string  & {});

/////////////////////////////////////////////////////////////////////

class Stream<T> {
    name: string
    constructor(name: string){
        this.name = name
    }
    getLastValue() {
        return {} as T // dummy impl
    }
}

class Registry {
    subscribeToTelemetry<T extends keyof TypeMap>(name: T): Stream<TypeMap[T]>;
    subscribeToTelemetry<T extends NonNullable<{}>>(name: EntityName<T>): Stream<T>;
    subscribeToTelemetry<T extends NonNullable<{}>>(name: EntityName<T>): Stream<T> {
        return new Stream<T>(name); 
    }
}

const r = new Registry();
// should suggest: ['mavic2' | 'ranger2' | 'ranger1'] since no type T is provided // OK
const myStream1 = r.subscribeToTelemetry('ranger1'); // myStream1 type will be Stream<RangerTelemetry>

// should suggest: ['mavic2' | 'ranger2' | 'ranger1'] since no type T is provided // OK
const myStream2 = r.subscribeToTelemetry('ranger2'); // myStream2 type will be Stream<RangerTelemetry>

 // should suggest only: ['mavic1' | 'mavic2' | 'mavic3'] since type T is provided as DroneTelemetry,
 // but ['mavic1' | 'mavic2' | 'mavic3', 'ranger1', 'ranger2', 'ranger3'] is shown instead.                // WRONG
const myStream3 = r.subscribeToTelemetry<DroneTelemetry>('mavic1'); // myStream3 type will be Stream<RangerTelemetry>

Вот тс-площадка с полным кодом

Привет @jcalz, спасибо за твои предложения. Я включил исходный код минимального примера моей проблемы. В комментариях вы можете увидеть ожидаемое поведение и то, что я получаю.

Humberto 29.08.2024 16:32

NonNullable<{}> — это просто {}; пожалуйста, отредактируйте , чтобы просто использовать {}, поскольку это в лучшем случае отвлекает. • Ваша проблема: ms/TS#26892 , и вы получаете предложения от обеих перегрузок. Обходной путь, который я бы предложил, — избегать перегрузок и вместо этого использовать общие условные типы, как показано в этой ссылке на игровую площадку. Это полностью решает вопрос? Если да, то я напишу ответ; если нет, то что мне не хватает?

jcalz 29.08.2024 16:51

@jcalz Я заглянул в вашу игровую площадку, и она почти идеальна, проблема в том, что, когда тип не указан, предполагаемым типом является Stream<'mavic2'> вместо Stream<DroneTelemetry>. Спасибо за помощь, постараюсь все исправить 👍🏼

Humberto 29.08.2024 16:51

Извините, посмотрите ссылку на эту игровую площадку. Это работает сейчас? Стоит ли писать ответ или я что-то еще упускаю?

jcalz 29.08.2024 16:54

привет @jcalz, извини за задержку, обязанности отца. Я только что проверил ваше последнее решение, все работает как положено 👌. Вы можете написать ответ

Humberto 29.08.2024 21:15

Давайте продолжим обсуждение в чате.

Humberto 29.08.2024 21:40

Нет, спасибо, я не хочу продолжать обсуждение этого вопроса, или, по крайней мере, не вести его в полуприватном чате здесь.

jcalz 29.08.2024 21:43
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
7
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Мы можем исправить это, изменив подход к сопоставлению типов друг с другом. Это будет лучше видно в коде, чем объяснено. При таком подходе вам не нужно перегружать функцию.

Краткое изложение подхода:

  • Индексируйте TypeMapRecord<> or {[x in key]: value}, где имена дронов имеют значение DroneTelemetry и то же самое для имен рейнджеров).
  • Определите два общих типа для subscribeToTelemetry
    • T — тип сущности DroneTelemetry|RangerTelemetry
    • F — это имя сущности, основанное на T, которое, если его не сузить, ни по определению, ни по умозаключению, будет именами всех сущностей. Когда он сужается с помощью проходного T, благодаря EntityName к типу объекта относятся только ключи. Это полезно не только для автозаполнения параметров, но и для типа возвращаемого значения, как указано в следующем пункте.
  • Положитесь на передачу ключа FTypeMap, чтобы получить возвращенный EntityType
    • Если T не пройден. Значение F будет выведено, что приведет к соответствующему типу
    • Если передается тип сущности T, мы не хотим передавать F (слишком сумбурно), поэтому у нас есть тип по умолчанию, который сужается только до имен, относящихся к этому типу сущности, а это означает, что мы сузили ключ, который мы перейти к TypeMap, вернув соответствующий EntityType

Следует отметить одну вещь: второй универсальный тип F выводится, когда мы не передаем T, но когда мы передаем T, мы должны передать F, потому что вы не можете передать некоторые общие типы и вывести остальные, либо все, либо ничего. . Вот почему нам нужно задать F тип по умолчанию.

type DroneTelemetry = { kind: 'DroneTelemetry', batteryLevel: number, velocity: number, location: { latitude: number, longitude: number } }
type RangerTelemetry = { kind: 'RangerTelemetry', velocity: number, location: { latitude: number, longitude: number } }

type EntityType = DroneTelemetry | RangerTelemetry;

const entityNames = {
    RangerTelemetry: ['ranger1', 'ranger2', 'ranger3'],
    DroneTelemetry: ['mavic1', 'mavic2', 'mavic3'],
} as const;

type EntityNameMap = typeof entityNames;
type EntityName<T extends EntityType> = EntityNameMap[T['kind']][number]

type TypeMap = Record<EntityName<RangerTelemetry>, RangerTelemetry> &
    Record<EntityName<DroneTelemetry>, DroneTelemetry>

class Stream<T extends EntityType> {
    name: string
    constructor(name: EntityName<T>) {
        this.name = name
    }
    getLastValue() {
        return {} as T // dummy impl
    }
}

class Registry {
    subscribeToTelemetry<T extends EntityType, F extends EntityName<T> = EntityName<T>>(name: F): Stream<TypeMap[F]> {
        return new Stream<TypeMap[F]>(name);
    }
}

const r = new Registry();
// Name options are all entity names
const myStream1 = r.subscribeToTelemetry('mavic2');
//        ^?const myStream2: Stream<DroneTelemetry>
const myStream2 = r.subscribeToTelemetry('ranger2');
//       ^?const myStream2: Stream<RangerTelemetry>

// Options are only drone names
const myStream3 = r.subscribeToTelemetry<DroneTelemetry>('mavic2');
//       ^?const myStream3: Stream<DroneTelemetry>

Детская площадка

Здравствуйте @Brother58697, спасибо за ваше время и решение. Интересный подход, который вы использовали 👌, он упрощает код.

Humberto 29.08.2024 21:24

В любое время @Умберто :)

Brother58697 29.08.2024 21:41

Другие вопросы по теме

Похожие вопросы

Можно ли заставить TypeScript понять следующее назначение общего объекта?
Тип переменной Typescript не меняется даже при строго типизированных манипуляциях
Почему функции Contravariance с их параметрами в машинописном тексте?
Получите доступ к этому в функции, которая является свойством объекта
Создание сопоставлений на основе перечислений, включая обеспечение ошибок компилятора
Почему сопоставление массива не работает с той же записью сопоставления рекламы?
React Router обнаружил следующую ошибку во время рендеринга. Ошибка: отрисовано меньше перехватчиков, чем ожидалось
Почему машинописный текст не позволяет частично указывать аргументы типа, чтобы создать новую универсальную функцию из другой универсальной функции?
Компонент React со свойством «as», которое указывает на другой компонент и также наследует его свойства
Введите параметры из параметров функции