在这种情况下如何获得类型安全的函数返回值?

问题描述 投票:0回答:1

在下面的代码示例中,我希望我的 IDE 接受

mySap.rootValues[0].test2
mySap.rootValues[1].test4
为有效。现在还没有,因为
rootValues[0]
rootValues[1]
属于
TestRootOutput | TestRootOutput2
类型。

type RootFunction<T, U> = (input: T) => U;

type Root<T, U> = {
  value: U;
};


const root = <T, U>(rootFn: RootFunction<T, U>): ((initialValue: T) => Root<T, U>) => {
  return initialInput => {
    let _value: U = rootFn(initialInput);

    return {
      get value() {
        return _value;
      },
    };
  };
};

interface TestRootInput {
  test: string;
}

interface TestRootOutput {
  test2: string;
}

interface TestRootInput2 {
  test3: string;
}

interface TestRootOutput2 {
  test4: string;
}

const testRoot = root<TestRootInput, TestRootOutput>(input => {
  return {test2: input.test};
});

const testRoot2 = root<TestRootInput2, TestRootOutput2>(input => {
  return {test4: input.test3};
});


type BranchSap = {roots: Root<any, any>[]};

const sap = <T extends BranchSap>(sap: T) => {
  const roots = sap.roots;

  return {
    rootValues: roots.map((root) => root.value as any as T['roots'][number]['value']),
  };
};
const mySap = sap({roots: [testRoot({test: 'hi'}), testRoot2({test3: 'hi2'})]});

// should be valid
console.log(mySap.rootValues[0].test2)

// should be valid
console.log(mySap.rootValues[1].test4)
javascript arrays typescript generics types
1个回答
0
投票

您的

sap()
函数的返回类型为
{rootValues: Array<T['roots'][number]['value']>}
类型。但是
T['roots'][number]
roots 数组的所有元素类型的
union
,它完全无法追踪哪些值在数组中的位置,并且
Array
是无序同构数组类型,并且不对可以在哪个索引处找到哪些类型进行编码。所以你已经抛弃了
T['roots']
中有关元素长度和位置的信息。

相反,您需要

rootValues
成为依赖于 T['roots']
元组类型
。首先,我们要确保
T['roots']
可能会被推断为一个元组。为此,我们可以将
T
设为
const
类型参数
:

const sap = <const T extends BranchSap>(sap: T): { ⋯ } => ⋯;

然后,我们想要获取

T['roots']
并将其 映射到新的元组类型。也就是说,我们创建一个 mapped type
I
的索引
T['roots']
处的每个元素转换为
T['roots'][I]['value']

type RootVal<T extends Root<any, any>[]> =
  { [I in keyof T]: T[I]['value'] };

const sap = <const T extends BranchSap>(
  sap: T
): { rootValues: RootVal<T['roots']> } => ⋯;

我们从调用者的角度来测试一下:

const mySap = sap({
  roots: [
    testRoot({ test: 'hi' }),
    testRoot2({ test3: 'hi2' })
  ]
});

如果您使用 IntelliSense 检查,您会看到

T
被推断为

{ readonly roots: [
     Root<TestRootInput, TestRootOutput>, 
     Root<TestRootInput2, TestRootOutput2>
   ];
}

由于

const
类型参数。因此输出的类型为

{ rootValues: [TestRootOutput, TestRootOutput2]; }

因为这就是当您使用

RootVal
映射输入元组时得到的结果。现在
mySap
的行为完全如您所愿:

mySap.rootValues[0].test2.toUpperCase(); // okay
mySap.rootValues[1].test4.toUpperCase(); // okay

请注意,这与sap()

实现
关系不大。大多数情况下,您只需要使用类型断言来说服编译器该实现满足调用签名:

const sap = <const T extends BranchSap>(
  sap: T
): { rootValues: RootVal<T['roots']> } => {
  const roots = sap.roots;
  return {
    rootValues: roots.map((root) => root.value),
  } as any; // <-- assert here or wherever you want
};

这是因为

map()
数组方法的 TypeScript 类型不会生成元组类型。事实上,让 TypeScript 自动完成这样的事情是不可能的,因为 TypeScript 类型系统缺乏必要的表达能力来表示它,请参阅将元组类型值映射到不同的元组类型值而不进行强制转换

在实践中,大多数通用函数无法被编译器验证为安全,因为它缺乏分析除少数特定操作(如索引)之外的所有操作的能力。因此,您通常需要类型断言等。

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.