我想定义一个函数,它接受一个字符串或字符串数组,并对它们做一些事情,例如将它们大写。
如果函数接受一个字符串,它应该返回一个字符串。并且,如果它接受一个字符串数组,它应该返回一个字符串数组。
这是我失败的尝试:
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[]'
}
你会如何解决这个问题?
为了做到这一点,你应该重载你的函数:
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[]
游乐场
如果您对更复杂的类型感兴趣,请考虑以下示例:
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"]
游乐场
请参阅docs
注:在实现中不允许返回泛型Params
,因为它被认为是不安全的行为。请看这个答案和/或我的文章
认为不安全例子:
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
如果你允许强制转换,对我来说,当我在返回值上添加强制转换时,错误就消失了
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();
}
}
不需要精确地采用这种方法;根据你的喜好调整。
问题
让我们看一下每个错误消息的关键部分:
'Params'可以用约束'string | 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
类型,但并非所有string
类型都是'foo'
类型。
同样,如果你传递一个元组:
const tuple: ['foo'] = ['foo'];
uppercase(tuple)
- TypeScript Playground
uppercase
的类型将变成:
const uppercase: <["foo"]>(params: ["foo"]) => ["foo"]
然而map
返回string[]
。虽然['foo']
类型是string[]
类型,但并非所有string[]
类型都是['foo']
类型。
在这两种情况下你都不需要约束类型。
<标题>(部分)解决方案可以使用函数重载来确保正确的返回类型与正确的实参类型配对:
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
标题>这种人喜欢把自己复杂化。有很多方法可以解决这个问题,但最简单的
if (typeof (param as string)==="string") {
或者另一个是
if (Array.isArray(param)) {}
不比较它是否为字符串,而是检查它是否为数组