【问题标题】:TypeScript: 'string' is assignable to the constraint of type T, but T could be instantiated with a different subtype of constraint 'string | string[]'TypeScript:'string' 可分配给类型 T 的约束,但 T 可以用约束 'string | 的不同子类型实例化细绳[]'
【发布时间】:2021-12-10 02:20:18
【问题描述】:

我想定义一个函数,它接受一个字符串或一个字符串数组并用它们做一些事情,例如大写它们。

如果函数接受一个字符串,它应该返回一个字符串。而且,如果它需要一个字符串数组,它应该返回一个字符串数组。

这是我失败的尝试:

const uppercase = <Params extends string | string[]>(params: Params): Params => {
  if (typeof params === "string") {
    return params.toUpperCase(); // ERROR: 'string' is assignable to the constraint of type 'Params', but 'Params' could be instantiated with a different subtype of constraint 'string | string[]'.
  }

  return params.map(param => param.toUpperCase()); // ERROR: 'string[]' is assignable to the constraint of type 'Params', but 'Params' could be instantiated with a different subtype of constraint 'string | string[]'
}

你会如何解决这个问题?

【问题讨论】:

  • 如果直接使用类型联合(而不是使用/定义参数)会发生什么? “不同的子类型” 看起来很有趣。
  • 它不会表达我的意图,因为它允许您传递 string 并返回 string[]

标签: typescript typescript-generics


【解决方案1】:

为了做到这一点,你应该重载你的函数:

const toUppercase = (str: string) => str.toUpperCase();

function uppercase(params: string[]): string[]
function uppercase(params: string): string
function uppercase<Params extends string | string[]>(params: Params) {
  return typeof params === "string" ? toUppercase(params) : params.map(toUppercase)

}

const str = uppercase('hello') // string
const arr = uppercase(['hello']) // string[]

Playground

如果您对更复杂的类型感兴趣,请考虑以下示例:

const toUppercase = <Str extends string>(str: Str) => str.toUpperCase() as Uppercase<Str>;

type TupleUppercase<
  T extends string[],
  Accumulator extends string[] = []
  > =
  (T extends []
    ? Accumulator
    : (T extends [infer Head, ...infer Tail]
      ? (Head extends string
        ? (Tail extends string[]
          ? TupleUppercase<Tail, [...Accumulator, Uppercase<Head>]>
          : never)
        : never)
      : never)
  )


function uppercase<Param extends string, Params extends Param[]>(params: [...Params]): TupleUppercase<Params>
function uppercase<Param extends string>(params: Param): Uppercase<Param>
function uppercase<Params extends string | string[]>(params: Params) {
  return typeof params === "string" ? toUppercase(params) : params.map(toUppercase)

}

const str = uppercase('hello') // "HELLO"
const arr = uppercase(['hello', 'world']) // ["HELLO", "WORLD"]

Playground

请看docs

附:你不能在你的实现中返回泛型Params,因为它被认为是不安全的行为。请参阅this answer 和/或我的article

考虑这个UNSAFE示例:

const toUppercase = (str: string) => str.toUpperCase();

function uppercase<Params extends string | string[]>(params: Params): Params {
  return typeof params === "string" ? toUppercase(params) : params.map(toUppercase)
}

uppercase('hello') // 'hello' but 'HELLO' in runtime

【讨论】:

  • 非常感谢您的回答!!您能否在实现中删除 Params 并简单地使用 function uppercase(params: string | string[]) { ... }
  • 您可以从第一个实现中删除Params,但不能从第二个实现中删除。为了推断参数,您应该提供通用参数
【解决方案2】:

如果您允许强制转换,对我来说,当我在返回值上添加强制转换时,错误就消失了

const uppercase = <Params extends string | string[]>(params: Params): Params => {
    if (typeof params === "string") {
      return params.toUpperCase() as Params; 
    }
  
    return params.map(param => param.toUpperCase()) as Params; 
  }

编辑

您使用 if 检查类型。这让您可以肯定地知道:返回的对象与参数的类型相同。但 TS 不知道这一点。那个[返回的字符串暗示收到的字符串]。

我认为您可以使用类泛型完成与您想要的类似的操作。

const uppercase = <Type extends string|string[]>(params: Type, converter: UpperCaser<Type>) => {
    return converter.toUpperCase(params);
}

abstract class UpperCaser<Param> {
    abstract toUpperCase(param: Param): Param;
}

class StringArray extends UpperCaser<string[]> {
    toUpperCase(stringArray: string[]): string[] {
        return stringArray.map(param => param.toUpperCase());
    }
}

class StringObj extends UpperCaser<string> {
    toUpperCase(string: string): string {
        return string.toUpperCase();
    }
}

不需要正是这种方法;根据自己的喜好调整。

【讨论】:

  • 通过这种方法,您可以将return params.toUpperCase() as Params; 更改为return [params.toUpperCase()] as Params;,TypeScript 会很高兴。如果 TypeScript 知道 paramsstring 但返回类型是 string[],我希望 TypeScript 抱怨,反之亦然。
  • 知道了。这个帮助?
【解决方案3】:

问题

我们来看看每条错误信息的关键部分:

'Params' 可以用不同的约束子类型'string | 来实例化。字符串[]'

Params 可以使用不同的约束子类型 string | string[] 实例化的示例是什么?

const foo = 'foo';

uppercase(foo);

——TypeScript Playground

如果您将鼠标悬停在 foo 变量的声明上,您会看到它具有以下类型:

const foo: 'foo'

然后,如果您将鼠标悬停在对 uppercase 的调用上,您会看到它具有以下类型:

const uppercase: <"foo">(params: "foo") => "foo"

类型 'foo' 扩展 string 所以它满足通用约束。然而你的函数的返回类型现在是'foo'toUpperCase 返回string。虽然'foo' 类型是string 类型,但并非所有strings 都是'foo's。

类似地,如果你传递一个元组:

const tuple: ['foo'] = ['foo'];

uppercase(tuple)

——TypeScript Playground

uppercase 的类型将变为:

const uppercase: <["foo"]>(params: ["foo"]) => ["foo"]

但是map 返回string[]。虽然['foo'] 类型是string[],但并非所有string[]s 都是['foo']s。

在这两种情况下,您都不需要约束类型。

(部分)解决方案

您可以使用function overloads 来确保正确的返回类型与正确的参数类型配对:

function uppercase(params: string): string;
function uppercase(params: string[]): string[];
function uppercase(params: string | string[]): string | string[] {
  if (typeof params === "string") {
    return params.toUpperCase();
  }

  return params.map(param => param.toUpperCase());
}

——TypeScript Playground

我知道你希望你的函数返回实现也被类型检查(即你不希望当字符串是参数时返回一个数组,反之亦然)但是我不相信是可能的[reference],至少有重载并且没有重大变化 - 我看到有一个使用类的答案。虽然我在这里提到的解决方案不会对实现进行检查,但它会在调用站点上进行检查,这是一些价值:

const uppercaseString = uppercase('foo');
const uppercaseArray = uppercase(['foo']);

// Attempt to call Array.prototype.map on a string
uppercaseString.map();
                ^^^
Property 'map' does not exist on type 'string'.

// Attempt to call String.prototype.substring on an array
uppercaseArray.substring();
               ^^^^^^^^^
Property 'substring' does not exist on type 'string[]'.

——TypeScript Playground

【讨论】:

    【解决方案4】:

    这些人喜欢把自己复杂化。有很多方法可以解决这个问题,但最简单的方法

    if (typeof (param as string)==="string") {
    

    或者另一个是

    
    if (Array.isArray(param)) {}
    

    而不是比较它是否是一个字符串检查它是否是一个数组

    【讨论】:

      猜你喜欢
      • 2019-12-13
      • 1970-01-01
      • 2020-11-25
      • 2021-06-19
      • 2023-01-11
      • 2021-10-14
      • 2022-01-20
      • 2021-02-13
      • 2021-11-16
      相关资源
      最近更新 更多