可以使用约束“对象”的其他子类型实例化

时间:2019-06-08 10:35:11

标签: typescript

A在递归类型中有类型检查错误。

我正在尝试为react-jss样式对象编写类型。

type StylesFn<P extends object> = (
  props: P
) => CSS.Properties<JssValue<P>> | number | string;

type JssValue<P extends object> =
  | string
  | number
  | Array<string | number>
  | StylesFn<P>;

// @ts-ignore
interface StylesObject<K extends string = any, P extends object = {}>
  extends Styles {
  [x: string]: CSS.Properties<JssValue<P>> | Styles<K, P>;
}
export type Styles<K extends string = any, P extends object = {}> = {
  [x in K]: CSS.Properties<JssValue<P>> | StylesObject<any, P> | StylesFn<P>
};

它可以正常工作,但打字稿会写错误。我使用@ ts-ignore,但这并不花哨

ERROR 24:11  typecheck  Interface 'StylesObject<K, P>' incorrectly extends interface 'Styles<any, {}>'.
  Index signatures are incompatible.
    Type 'Properties<JssValue<P>> | Styles<K, P>' is not assignable to type 'StylesFn<{}> | Properties<JssValue<{}>> | StylesObject<any, {}>'.
      Type 'Properties<JssValue<P>>' is not assignable to type 'StylesFn<{}> | Properties<JssValue<{}>> | StylesObject<any, {}>'.
        Type 'Properties<JssValue<P>>' is not assignable to type 'Properties<JssValue<{}>>'.
          Type 'JssValue<P>' is not assignable to type 'JssValue<{}>'.
            Type 'StylesFn<P>' is not assignable to type 'JssValue<{}>'.
              Type 'StylesFn<P>' is not assignable to type 'StylesFn<{}>'.
                Type '{}' is not assignable to type 'P'.
                  '{}' is assignable to the constraint of type 'P', but 'P' could be instantiated with a different subtype of constraint 'object'.

此错误是什么意思?

4 个答案:

答案 0 :(得分:5)

补充上面的好答案。


缩短答案

TLDR; 造成这种错误消息的主要原因有两个:

原因1::当您为通用Type Parameter分配“具体类型”时。这在Typescript中是非法的。

问题

const func1 = <A extends string>(a: A = 'foo') => `hello!` // Error!

const func2 = <A extends string>(a: A) => {
    //stuff
    a = `foo`  // Error!
    //stuff
}

解决方案

const func1 = <A extends string>(a: A) => `hello!` // ok

const func2 = <A extends string>(a: A) => { //ok
    //stuff
    //stuff
}

原因2:如果您错误地在类中重复了Type Parameter,请键入或界面。

问题:

interface Foo<A> {
    //bellow 'A' is duplicated in relation to above 'A'
    map: <A,B>(f: (_: A) => B) => Foo<B>
}

const makeFoo = <A>(a: A): Foo<A> => ({
   map: f => makeFoo(f(a)) //error!
})

解决方案:

interface Foo<A> {
    // duplication removed
    map: <B>(f: (_: A) => B) => Foo<B>
}

const makeFoo = <A>(a: A): Foo<A> => ({
   map: f => makeFoo(f(a)) //ok
})

长答案


了解错误消息

我要遵循的方法是分解错误消息的每个元素:

Type '{}' is not assignable to type 'P'.
  '{}' is assignable to the constraint of type 'P', but 'P' could be
 instantiated with a different subtype of constraint'object'
  

注意::在将来的TS will change上,错误消息从'{}'到unknown。他们发现使用'{}'只会使人们感到困惑。


名称约定

我将在所有其他示例中使用波纹管功能结构:

const func = <A extends B>(a: A) => `hello!`

让我们使用这个名称约定:

  • 'A'-> Type Parameter
  • 'B'-> Type Constraint(编译器称其为:“ A”类型的约束)

什么是different subtype

这是定义:

两种类型相等:如果它们具有完全相同的属性和方法,则不多不少

两种类型不同:如果它们不相等。

类型A是类型B的子类型:如果类型A与类型B等于 而且还会添加更多信息(包括更多属性和/或方法)。

请注意,概念重叠。两种类型可以是特定类型的子类型,并且同时被认为彼此不同(不同子类型)。

这里是一个例子:

// An arbitrary type
type Foo = {
    readonly prop1: `bar`
}

// A subtype of 'Foo'
type SubType = {
    readonly prop1: `bar`
    readonly prop2: `thux` //new property introduced
}

// A different subtype of 'Foo' (but still a subtype)
type DiffSubType = {
    readonly prop1: `bar`
    readonly prop3: `different` //new property introduced, but different that the property 'prop2' introduced up-above
}

请注意,我们可以创建特定类型几乎无限的 个子类型。如您所见,这是编译器消息的核心。


什么是{}

类型{}表示“具有属性和/或方法”的任何类型。换一种说法, 类型{}对应于您可以想象的除nullundefined之外的任何类型。

请记住,在Javascript中,某些原语,例如'string','numbers'和函数(nullundefined除外)也具有相关的属性和方法。 (例如:'hello world'.length()myFunction.bind(...)

让我们检查一下。假设存在以下实例:

const foo: Foo = {...} // definition omited for brevity
const foo_SubType: SubType = {...}
const foo_DiffSubType: DiffSubType = {...}

因此,我们可以通过执行以下操作来验证代表“ {}”类型的内容:

// function definition
const func = <A extends {}>(a: A) => `hello!`

// many ways to call it
const c0 = func(undefined) // error: Argument of type 'undefined' is not assignable to parameter of type '{}'
const c1 = func(null) // error: Argument of type 'null' is not assignable to parameter of type '{}'
const c2 = func(() => undefined) // ok
const c3 = func(10) // ok
const c4 = func(`hi`) // ok
const c5 = func({}) //ok
const c6 = func(foo) // ok
const c7 = func(foo_SubType) //ok
const c8 = func(foo_DiffSubType) //ok


TYPE CONSTRAINT工作原理

为说明起见,我将向您展示三种情况。在每种情况下唯一会改变的是Type Constraint,其他都不会改变。

请注意,Type ConstraintType Parameter施加的限制仅包含 不同类型,不包括不同子类型。我们来看一下:

无限制

const func = <A>(a: A) => `hello!`

// call examples
const c0 = func(undefined) // ok
const c1 = func(null) // ok
const c2 = func(() => undefined) // ok
const c3 = func(10) // ok
const c4 = func(`hi`) // ok
const c5 = func({}) //ok
const c6 = func(foo) // ok
const c7 = func(foo_SubType) //ok
const c8 = func(foo_DiffSubType) //ok

某些限制

请注意,限制不影响子类型。

在打字稿中,Type Constraint仅限制了上限,它没有告诉您Type Parameter的精确度

const f = <A extends Foo>(a: A) => `hello!`

// call examples
const c0 = func(undefined) // error
const c1 = func(null) // error
const c2 = func(() => undefined) // error
const c3 = func(10) // error
const c4 = func(`hi`) // error
const c5 = func({}) // error
const c6 = func(foo) // ok
const c7 = func(foo_SubType) //ok  <-- Allowed
const c8 = func(foo_DiffSubType) //ok <-- Allowed

更多约束

const func = <A extends SubType>(a: A) => `hello!`

// call examples
const c0 = func(undefined) // error
const c1 = func(null) // error
const c2 = func(() => undefined) // error
const c3 = func(10) // error
const c4 = func(`hi`) // error
const c5 = func({}) // error
const c6 = func(foo) // error <-- restricted now
const c7 = func(foo_SubType) //ok  <-- Still allowed
const c8 = func(foo_DiffSubType) //ok <-- NO MORE ALLOWED !


结论

将具体类型分配给泛型Type Parameter是不正确的,因为Type Parameter可以总是实例化为某些任意不同的子类型:

代码:

const func = <A extends Foo>(a: A = foo_SubType) => `hello!`

产生此错误消息:

Type 'SubType' is not assignable to type 'A'.
  'SubType' is assignable to the constraint of type 'A', but 'A'
could be instantiated with a different subtype of constraint 
'Foo'.ts(2322)

解释:

在函数'func'中,Type Parameter'A'不仅可以接受Type Constraint'Foo',还可以接受其所有可能的子类型(其中的无限数量)。

当您说要将Type Parameter'A'缺省设置为'Foo'类型的 specific 子类型时(在上面的示例中为A = foo_SubType),那么我必须提醒您,则Type Parameter'A'可以接受Type Constraint'Foo'的许多不同子类型,而不能您指定的特定子类型。

解决方案:

const func = <A extends Foo>(a: A) => `hello!`

Source code used in this answser

答案 1 :(得分:3)

该错误警告,由于通用类型P可以是更定义(或受限制)的类型,因此无法为您的通用类型{}分配P

这意味着值{}将不能满足可用于通用类型P的所有可能的类型。

例如,我可以有一个像这样的泛型(同样的错误):

function fn<T extends boolean>(obj: T = false) {
}

,您可以拥有一个比布尔型更具体的类型:

type TrueType = true;

,如果将其传递给泛型函数fn:

const boolTrue: True = true;
fn(boolTrue);

将false分配给false时,即使TrueType遵守泛型T的扩展布尔值

有关此错误消息的更多上下文,请参阅提示此错误消息https://github.com/Microsoft/TypeScript/issues/29049的问题。

答案 2 :(得分:1)

最小示例

使用缩短版本会产生相同错误的问题会更加明显:

interface StylesObject<P extends object = {}> extends Styles {
//        ^~~~~~~~~~~^ same error as in question
    foo: (props: P) => void;
}

type Styles<P extends object = {}> = {
    foo: (props: P) => void
};

错误(请查看See it here at regex101,了解完整的消息堆栈)

'{}'可分配给类型'P'的约束, (a) ,但'P'可以使用约束'object'的另一个子类型实例化 (b)


出什么问题了?

  1. StylesObject必须是Styles的子类型(兼容)。
  2. 通过编写extends Styles,我们没有为Styles设置泛型类型参数。因此,P将使用 default {}类型的实例化。
  3. StylesObject<P> 有效地想要从Styles<{}>扩展,但是两个不兼容
const myStylesObject: StylesObject<{ foo: string }> = ...;
const styles: Styles<{}> = myStylesObject // error: incompatible

原则上,StylesObject允许扩展约束object的任何参数类型(默认= {}在这里不重要)。并且Styles<{}>将与object兼容。这是(a)所说的错误部分。

但是,如果Pobject的更窄子类型,如上面的代码中的myStylesObject呢?它不再工作了。这是(b)部分所说的错误。

狗和动物的比喻

PlaygroundPlayground
const playWithDog = (dog: Dog) => { dog.isBarking = true }
const handleAnimal: (animal: Animal) => void = playWithDog 
// error, cannot assign function that wants to deal with dogs (specific animals) 
// to a variable type that describs a callback for all sorts of animals.

function feedAnimal(animalFeeder: (animal: Animal) => void) { }
feedAnimal((dog: Dog) => { dog.isBarking = true })
// Error: Type 'Animal' is not assignable to type 'Dog'.

解决方案

选项1:为StylesObject使用类型别名

type StylesObject<K extends string = any, P extends object = {}> = Styles<K, P> & {
    [x: string]: CSS.Properties<JssValue<P>> | Styles<K, P>;
}

StylesObject与以前的类型相同,是从Styles扩展为& /交点。优点:您现在可以声明further infosStyles<K, P>would be not possible with interface中的更多信息。

我建议使用此变体,因为不需要其他更改。看看this answer

选项2:在StylesFn中使用方法声明

type StylesFn<P extends object> = {
    create(props: P): CSS.Properties<JssValue<P>> | number | string
}

这要求StylesFn是带有方法声明的对象类型,例如createPlaygroundPlayground

答案 3 :(得分:0)

简短的解释。

抛出错误的示例:

type ObjectWithPropType<T> = {prop: T};

// Mind return type - T
const createCustomObject = <T extends ObjectWithPropType<any>>(prop: any): T => ({ prop });

type CustomObj = ObjectWithProp<string> & { id: string };

const customObj = createCustomObj<CustomObj>('value'); // Invalid
// function will only ever return {prop: T} type.

这里的问题是返回对象将仅与属性prop匹配,而与其他任何属性都不匹配。扩展ObjectWithPropType会产生错误的类型约束。该示例完全是错误的方法,仅用于说明对象属性的实际冲突。

如何在创建函数中约束子类型:

type StringPropObject = ObjectWithPropType<string>

const createCustomObject = <T>(prop: T extends ObjectWithPropType<infer U> ? U : T): ObjectWithPropType<T> => ({ prop });

const stringObj = createCustomObject<StringPropObject>('test');

在这种情况下,该函数要求参数为字符串。该对象仅具有prop属性,该函数会返回所需的形状。