Pull to refresh

Comments 18

Извращение какое-то. Проще же:

const format = (input: string | number | boolean) => {
    switch (typeof input) {
        case 'string':
            return formatters.string(input);
        case 'number':
            return formatters.number(input);
        case 'boolean':
            return formatters.boolean(input);
    }
};

Конкретно в этом случае - да, но в общем случае в ts и правда наблюдаются проблемы. Особенно если ещё и дженерики сюда подключить...

Да нет там проблем. Просто вы снова пишите на JS вместо того, чтобы писать на типизированном языке.

Возьмите какой-нибудь Rust и попробуйте такое выкинуть. Он вам расскажет что о вас думает :)

Серьезно. Попишите на языке без возможности обмана через "as any", это даст хороший буст в понимании как с такими кейсами работать. А еще лучше какой-нибудь хитрожопый JSON поворчайте на чем-то типизированном.

В том-то и дело, что на Rust я могу делать те вещи, которые на Typescript так просто не сделаешь.

К примеру, тот же format в Rust реализуется вообще без проверок типа в рантайме, через трейты. Но на Typescript нет возможности создать трейт и объявить его реализацию для типа, что периодически и вынуждает заниматься чем-то подобным тому, что делается в статье.

Действительно, со свичем будет ещё и безопаснее.

С другой стороны, что мешает вызвать нужную функцию прям на месте, где мы точно знаем с каким типом работаем?

На самом деле as any работает, только применять его надо не к input, а к formatter.

И тип возвращаемого значения у format будет также any. Тогда уже

const formatter = formatters[inputType] as (input: string | number | boolean) => string

или указать тип возвращаемого значения у format явно string

Ну да, тип возвращаемого значения у функции указать придётся. Впрочем, он не помешает при любом из подходов, потому я даже не смотрел есть он там или нет.

Здесь происходит ошибка, потому что TypeScript не может знать, какой именно тип будет у formatter в runtime. Это не ts не умный, это в принципе невозможно в compile time. В примере @Akuma явно делается проверка в switch (), поэтому происходит narrowing, и ts может гарантировать тип. Вы же просто "заткнули" ts, используя 'as never'. Вообще, если вы используете as, вы утверждаете, что здесь будет именно этот тип, даже если это в runtime не так. Попробуйте выполнить format({} as string) и ts ругаться не будет, а вот в runtime выскочит ошибка, в частности, formatter is not a function.

Ну уж нет, {} as string - это грубый обман typescript. А авторский input as never (как и мой formatter as any) - это обход ложноотрицательного результата тайп-чекера в корректном коде.

Это не ts не умный, это в принципе невозможно в compile time.

Да всё там возможно, но нужны завтипы. Или хотя бы механизмы для учёта корреляции типов двух переменных (однако, я не уверен что такой механизм будет проще завтипов). Однако в ts нет ни того, ни другого, и именно поэтому ts не умный.

Любой as type - это обман. Вы делаете то же самое в своем примере. Вы полагаете, что лучше знаете как этот код будет работать и что в вашем случае, что через formatter as any - просто убираете проверку типов.

Вы думаете, что на вход могут прийти только перечисленные типы, но TS достаточно умен, чтобы понять, что нифига это не так. Вдруг вы там тоже решите быть уменее и подадите {} as number? В таком случае ваш код упадет, а мой вариант без обманов - нет, он пройдет за switch, где вы корректно обработаете такое поведение.

Если вам кажется, что Typescript и правда пытается защитить вас от {} as number, предлагаю подумать, почему тип переменной input_type выводится Typescript не как string, а как 'string' | 'number' | 'boolean'

Здесь происходит ошибка, потому что TypeScript не может знать, какой именно тип будет у formatter в runtime. Это не ts не умный, это в принципе невозможно в compile time.

Цель комментария была в том, чтобы объяснить, как правильно "готовить ts". Имелось ввиду, что для ts это невозможно. Ts не может знать, какой именно тип у formatter потому как не знает, какой именно тип будет у input. В наборе formatters нет такой функции, которая сможет принять input c данным объединением string | number | union. Другими словами, значение обязано быть, но функция сможет принять только самый узкий тип, в данном случае, never. С точки зрения ts вызов никогда не произойдет, поэтому мы видим у formatter тип (input: never) => string. Использование as, когда "единственное, что работает" - плохая практика, может привести к искажению типов, потому как в ts as - это просто допущение, а не реальный type-casting. Некоторые так и работают, руководствуясь принципом "чтобы ts не ругался". Если же отказаться от asany), то как решать такие задачки? С помощью type guard, описанных в приведенной статье из документации.

В общем, если принять мысль, что задача кожаного мешка сделать так, чтобы ts помогал, то пусть хоть как-то, но сам выводит типы. При условии, что type guards будут корректными, это гарантирует вам и корректность типов.

Есть вариант и с as. Нужно описать полный интерфейс у format, т.е. задать тип возвращаемого значения, а в теле функции делайте что хотите.

{} as string - синтетический пример, чтобы показать "оторванность" ts от рантайма, в отличие от статически типизированных языков. Хотите реальный пример? Вы описали интерфейс api response и на основе этого интерфейса далее выводите типы. А вам вдруг пришел response немного с другим интерфейсом. Скажем, бэкенд-тима решила, что некоторое поле будет с типом null, а не undefined, а вам не сказали (ничеси, неужели такое бывает?..). Статически типизированный язык заставит проверить типы или закастить. В ts этот фокус пройдет, но далее по коду может свалиться.

А авторский input as never (как и мой formatter as any) - это обход ложноотрицательного результата тайп-чекера в корректном коде.

Input as never... As never... Бессмыслица какая-то...

Если же отказаться от as (и any), то как решать такие задачки? С помощью type guard, описанных в приведенной статье из документации.

Но ведь Type guard - такое же допущение, Typescript принципиально не проверяет корректность type guard.

В общем, если принять мысль, что задача кожаного мешка сделать так, чтобы ts помогал, то пусть хоть как-то, но сам выводит типы.

Ну так ведь автор именно так и поступил...

А вам вдруг пришел response немного с другим интерфейсом. Скажем, бэкенд-тима решила, что некоторое поле будет с типом null, а не undefined, а вам не сказали (ничеси, неужели такое бывает?..)

…то это будет ошибкой в клиенте к API, а вовсе не в функции format.

Используя статическую типизацию, мы всегда считаем что типы переменных указаны правильно и рантайм-значения не выходят за их пределы. Иначе в статической типизации попросту нет никакого смысла.

Input as never... As never... Бессмыслица какая-то...

Согласен, потому и предлагаю formatter as any

Ну так це можно рассчитать в compile time. Тк typeof может вернуть лишь три значения исходя из типа inputType, и все эти значения могут быть использованы как ключи для получения значения из formatters.

в TS'е для подобных случаев оставлена лазейка, но по какой то причине никто вообще не в курсе о её существовании
если чуть-чуть поправить код то всё будет работать

type formatInputMap = {
    "string": string,
    "number": number,
    "boolean": boolean,
}

const formatters: {
    [K in keyof formatInputMap]: (input: formatInputMap[K]) => string;
} = {
  string: (input: string) => input.toUpperCase(),
  number: (input: number) => input.toFixed(2),
  boolean: (input: boolean) => (input ? "true" : "false"),
};

function format<T extends keyof formatInputMap>(input: formatInputMap[T]) {
  const inputType = (typeof input) as T;
  const formatter = formatters[inputType];
  return formatter(input);
}

каст типа никуда не делся, но это уже проблема typeof тут мы бессильны

Как вариант без хардкода типов

const formatters = {
	string: (input: string) => input.toUpperCase(),
	number: (input: number) => input.toFixed(2),
	boolean: (input: boolean) => (input ? "true" : "false"),
}

const format = (input: string | number | boolean) => {
	const mappedFormatters: {
		[K in keyof typeof formatters]:
			typeof formatters[K] extends (...args: Parameters<typeof formatters[K]>) => ReturnType<typeof formatters[K]>
				? typeof formatters[K]
				: never
	} = formatters

	const inputType = typeof input as
		| "string"
		| "number"
		| "boolean"

	return (<TFormatter extends keyof typeof mappedFormatters, TInput extends Parameters<typeof mappedFormatters[TFormatter]>>(formatter: TFormatter, ...input: TInput) => {
		mappedFormatters[formatter](...input)
	})(inputType, input)
}

Sign up to leave a comment.

Articles