Условные типы и строковый литерал + взаимодействие классов - PullRequest
1 голос
/ 25 февраля 2020

Я столкнулся со странным взаимодействием в системе типов при попытке правильно набрать as опору от эмоций .

import React, { Component, FC, PropsWithChildren } from "react";

// Possible types for `as` prop are tag name or component
type AsType =
  | keyof JSX.IntrinsicElements
  | unknown


// Infered props based on `as`
type AsProps<T extends AsType> = {
  as?: T;
} & (T extends FC<infer FProps>
  ? FProps & { fc?: "fc" }
  : T extends new (...args: any) => Component<infer CProps>
  ? CProps & { cc?: "cc" }
  : T extends keyof JSX.IntrinsicElements
  ? JSX.IntrinsicElements[T] & { tag?: "tag" }
  : { non?: "non" });

const Box = <T extends AsType>(props: PropsWithChildren<AsProps<T>>) => null;

// Test components
class ClassComponent extends Component<{ CCProp: "foo" }> {}
const FunctionComponent: FC<{ FCProp: "foo" }> = () => null;

const Foo: FC = () => (
  <>
    <Box as={FunctionComponent} fc="fc" FCProp="foo" />
    <Box as={ClassComponent} cc="cc" CCProp="foo" />
    {/* Here types are inferred incorrectly.*/}
    {/* `tag` should be expected */}
    {/* `href` should show error that `true` is not assignable to `string` */}
    <Box as="a" tag="tag" href /> 
    <Box non="non" />
  </>
);

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

Я использую условные типы для ожидания правильного реквизита, основанного на том, что находится в подпорке as, и для строковых тегов система типов прямо вверх сдаться. Если вы укажете тег as const, это сработает. Если нет, он выводится как string, а условная часть AsProps выводит его как non. Во время возни я обнаружил, что по какой-то причине, например, если вы пропустите unknown из AsType, теги будут работать даже без as const.

Я не могу найти правильное решение.

1 Ответ

2 голосов
/ 25 февраля 2020

Основная проблема, с которой вы, похоже, сталкиваетесь, заключается в том, что компилятор использует некоторую эвристику для определения, принимать ли значение, подобное "a", и выводить его тип как строковый литерал "a", или стоит ли расширять его до string. Строковые литеральные значения, присвоенные не const переменным или параметрам функции, имеют тенденцию расширяться до string по умолчанию.

Как вы заметили, один из способов предотвратить это расширение - использовать const утверждение . Тип "a" as const всегда будет "a" и не будет расширен до string. Это работает в вашем случае, но накладывает бремя на абонент из Box, и вы, вероятно, хотели бы, чтобы это произошло, не требуя, чтобы люди, использующие Box, писали as const в любом месте.

Другой способ предотвратить расширение состоит в том, чтобы иметь значение в узле вывода общего типа c, которое было ограничено для типа, содержащего string или строковый литерал. (Вы можете прочитать больше о конкретных правилах в запросе на выборку, реализующем это поведение, microsoft / TypeScript # 10676 .) Так что если у вас есть declare function foo<T extends string>(x: T): void; и вызов foo("a"), то T будет выведено как "a". Но , если у вас bar<T extends unknown>(x: T): void; и звоните bar("a"), то T будет выводиться как string. Будет работать тип объединения, содержащий нечто, присваиваемое string, поэтому T extends string | number все равно даст вам "a".

Может быть, теперь вы думаете: «Хорошо, keyof JSX.IntrinsicElements | unknown должен быть объединением, содержащим нечто, присваиваемое string, поскольку keyof JSX.IntrinsicElements выглядит как "a" | "abbr" | "address" | "area" | "article" | ..., и каждый из них является строковым литералом". Ну, как вы также заметили, unknown действительно все испортило. Видите ли, unknown является верхним типом TypeScript . тип верха - универсальный супертип ; любой тип X будет подтипом unknown. И, следовательно, X | unknown будет эквивалентно unknown. Компилятор агрессивно упрощает любое объединение с unknown до unknown. И вы теряете "a" | "abbr" | ... ..., который больше не намекает компилятору на предотвращение расширения с "a" до string.

Итак, обходной путь здесь, вероятно, состоит в том, чтобы отказаться от явного unknown и вместо этого использовать что-то, что эквивалентно ему с точки зрения того, какие типы присваиваются, но который компилятор сохраняет как объединение и не агрессивно упрощать. До введения unknown вам нужно было использовать что-то вроде {} | undefined | null для обозначения «всех возможных типов», и это все еще работает:

type Unknown = {} | undefined | null; 

type AsType =
  | keyof JSX.IntrinsicElements
  | Unknown

И тогда вы получите желаемое поведение:

<Box as="a" tag="tag" href /> // error!
//                    ~~~~ <-- true is not a string

Хорошо, надеюсь, это поможет; удачи!

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...