Aetae
@Aetae
Тлен

Как конкретизировать сложный тип?

Допустим есть такой интерфейс(на самом деле он конечно гораздо сложней):
interface Some {
  value: string | string[];
}

Мне нужно получить константу полностью соответствующую этому типу, но конкретизированную по значению. Т.е. можно ли как-то заявить для ts, что структура будет именно такой и меняться не будет?

Если просто указать тип, то работать не будет, т.к. ts считает что структура объекта может измениться как угодно в пределах типа, что логично:
const foo: Some = {
  value: ['a', 'b'] // здесь всегда будет массив
};

foo.value.map(() => {});  //  Property 'map' does not exist on type 'string | string[]'


То что примерно мне нужно, я могу получить сделав функцию-обёртку:
function SomeType<T extends Some>(value: T): T {
  return value
}

const bar = SomeType({ 
  value: ['a', 'b'] // здесь всегда будет массив  
});

bar.value.map(() => {});  // ок

Но хотелось бы как-то правильно это делать.

...upd:
Что именно мне нужно:
a) при объявлении объекта он должен быть проверен на соответствие типу Some(чтоб ide мне сразу подсветила если я где-то ошибся и предложила автокомплит).
б) при последующей работе с объявленным объектом я не хочу уточнять в ручную каждый подтип каждого свойства и подсвойства возможного в типе Some, ts должен понять их из объявления.
с) объявленный объект должен всё ещё оставаться для ts вариантом типа Some, чтобы его можно было спокойно передавать в методы и функции ожидающие Some на вход.
д) объект должен оставаться мутабельным(в рамках конкретизированного типа, само собой).
  • Вопрос задан
  • 492 просмотра
Решения вопроса 1
Aetae
@Aetae Автор вопроса, куратор тега TypeScript
Тлен
И вот в ts 4.9 наконец-то ввели фичу отвечающую на этот вопрос (буквально джва года ждал):
interface Some {
  value: string | string[];
}

const foo = {
  value: ['a', 'b'] // здесь всегда будет массив
} satisfies Some;

foo.value.map(() => {}); // ок
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 3
TypeScript так не умеет.

Могу предложить нормализацию сложных типов. Что-то типа такого:
// Заготовка для Some
interface SomeDraft {
  value?: string | string[]; // Массив или значение. Может отсутствовать.
}

// Some имеет более строгий тип и как можно меньшую вариативность,
// чтобы в коде не делать проверки.
interface Some {
  value: string[];  // Всегда присутствует. Всегда массив.
}

function makeSome({ value = [] }: SomeDraft): Some {
  // Приводим к нормальному (более строгому) типу
  return {
    value: Array.isArray(value) ? value : [value],
  };
}

// Здесь Some задаётся через заготовку - так проще задавать значения
// Но результат всегда более строгий - так проще значениями пользоваться
const foo = makeSome({ value: 'foo' }); // { value: ['foo'] }

foo.value.map(() => {}); // foo.value - всегда массив и TypeScript это знает
Ответ написан
Вы какую-то странную задачу поставили. Если вам нужен более узкий тип - объявите его отдельно (и может быть даже объявите Some через этот более узкий тип) и пользуйтесь им, или даже вообще не указывайте тип в вашем примере.

UPD

объявленный объект должен всё ещё оставаться для ts вариантом типа Some, чтобы его можно было спокойно передавать в методы и функции ожидающие Some на вход.

Нижеследующий код более чем корректный:
interface Some {
  value: string | string[];
}

const foo = {
  value: ['a', 'b'] // здесь всегда будет массив
};

foo.value.map(() => { });

function f(a: Some) {
    return a;
}

f(foo);


при объявлении объекта он должен быть проверен на соответствие типу Some(чтоб ide мне сразу подсветила если я где-то ошибся и предложила автокомплит).

Значит всё-таки вам нужен не тип Some в месте объявления объекта, а более узкий тип, где value - всегда массив. Например:
interface Some {
  value: string | string[];
}

// Тут вам стоит придумать более подходящее по смыслу название
interface WithArrayValue {
    value: string[];
}

const foo: Some & WithArrayValue = {
    value: ['a', 'b'] // здесь всегда будет массив
};

foo.value.map(() => { });

function f(a: Some) {
    return a;
}

f(foo);
Ответ написан
@Interface
Поделитесь для чего вам это нужно? Я не спрашиваю про реальную задачу, а скорее про демонстрационный код, подобный тому что вы привели.

Единственная причина которая приходит мне в голову это: вам нужно объявить переменную, которая будет иметь более узкий тип чем Some, который вы хотите вывести автоматически, а не писать вручную, но при этом вы хотите иметь гарантию того итоговый тип является подтипом Some.

Как один из вариантов решения, могу предложить это:
interface Some {
    value: string | string[];
}

type Extends<E, T extends E> = T;

const foo = {
    value: ['a', 'b'] // здесь всегда будет массив
}
foo as Extends<Some, typeof foo>;

playground

То есть объявление переменной состоит из 2 выражений: объявление переменной с автоматическим выводом типа и валидация полученного типа.

P.s. к сожалению, просто foo as Some не отловит все, что нужно, например {value: ['a', 'b', 8]}
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

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