TypeScript 允许推断派生返回类型的类型别名

gudnpqoy  于 6个月前  发布在  TypeScript
关注(0)|答案(6)|浏览(45)

建议

🔍 搜索词

函数推断返回类型别名

✅ 可实施性检查清单

我的建议满足以下准则:

  • 这不会对现有的TypeScript/JavaScript代码造成破坏性的改变
  • 这不会改变现有JavaScript代码的运行时行为
  • 这可以在不根据表达式的类型发出不同的JS的情况下实现
  • 这不是一个运行时特性(例如库功能、带有JavaScript输出的非ECMAScript语法、JS的新语法糖等)
  • 这个特性将与TypeScript's Design Goals的其他部分一致。

⭐ 建议

如果一个函数能够为其返回类型内联声明一个类型别名,那将会非常方便。这种可能的语法如下:

const createCounter = (): infer Counter => {
  const result = {
    count: 0,
    increment: () => ++result.count,
  };
  return result;
};

这将自动创建一个类型别名,允许像下面这样使用:

import { Counter, createCounter } from './counter';

📃 激励示例

目前,类声明会自动为生成的构造函数函数发出一个类型。然而,如果使用标准函数创建对象,这是不可能的。函数的推断返回类型可以像下面这样进行别名:

// aliased type:
// type Counter = {
//   get: () => number;
//   increment: () => number;
// }
export type Counter = ReturnType<typeof createCounter>;

const createCounter = () => {
  const result = {
    count: 0,
    increment: () => ++result.count,
  };
  return result;
};

这是一个强大的模式,因为它允许实现成为类型的源引用,而不是相反的方式。然而,尽管别名类型是从函数派生的,但函数本身并不返回别名类型。

// type is:
// const counter: {
//   get: () => number;
//   increment: () => number;
// }
// NOT Counter
const counter = createCounter();

为了使用派生类型,有两种选择,两种都很笨拙:

  • 将责任交给调用者明确使用该类型:
import { Counter, createCounter } from './counter';

const counter: Counter = createCounter();
  • 创建一个多余的高阶函数:
export type Counter = ReturnType<typeof createCounterInternal>;

export const createCounter = (): Counter => createCounterInternal();

const createCounterInternal = () => {
  // counter impl
};

💻 用例

任何返回复杂对象的函数都可以从这个特性中受益。

kkbh8khc

kkbh8khc1#

诚然,我不知道这背后有多少复杂性,但作为一个语言特性,如果能将其用作泛型类型的内部别名,特别是在承诺的情况下,那将是非常好的:

const createCounter = async (): Promise<infer Counter> => {
  const result = {
    count: 0,
    increment: () => ++result.count,
  };
  return result;
};
oyt4ldly

oyt4ldly2#

我一直在研究一个JS项目,调查将其迁移到Typescript会有多大难度,这让我想起了这个功能请求,因为我注意到这个功能会让迁移变得容易得多。
基本上有大量的文件以这种模式创建对象:

export function coreContext() {
    var context = {};
    context.version = "1.0.0",
        context.someMethod = function (arg) {
            return "something"
        }
    // ...
    // lots of assignment to context
    // ...
    return context;
}

还有大量的不同文件也以类似的方式创建对象,但它们需要其他函数返回的对象:

//---- module1.js
export function doSomething(context){
    // ... some logic
}

//---- module2.js
export function doSomethingElse(context){
    // ... some logic
}

//---- module3.js
export function iThinkYouGetTheIdea(context){
    // ... some logic
}

忽略“expando赋值”模式(顺便说一下,我希望TS能更广泛地支持它),只需添加 infer CoreContext 来定义类型名称:

export function coreContext() infer CoreContext {
    var context = {};
    context.version = "1.0.0",
    context.someMethod = function(arg) {
        return "something"
    }
    //...
    // lots of assignment to context

    return context;
}

稍后使用它:

import { CoreContext } from './coreContext';
export function doSomething(context: CoreContext) {
    // ... some logic
}

对于这个JS到TS的迁移来说,这将是一个巨大的节省时间的方法。
编辑:
如果这样的构造也可以作为迁移过程中的中间步骤添加到JSDoc注解中,以及为当前的JS文件提供更好的IDE体验,那将会非常有用:

// @filename: coreContext.js
/**
* @returns {infer CoreContext}
*/
export function coreContext(){
    var context = {};
    //...
    return context;
}
// @filename: module2.js
/**
* Copies a variable number of methods from source to target.
* @param { import("./coreContext").CoreContext } context
*/
export function doSomethingElse(context){
    // ... some logic
}
iqih9akk

iqih9akk3#

并非我想在这里给自己的特性设个陷阱,但这是一个很大的范围扩展,因为你在那里使用的语法不是TypeScript目前可以推断出来的。重要的是要指出,在声明时,任何值的类型都是“固定”的。它不能像添加值一样迭代地扩展类型。
不过,这也是一个特性。我喜欢TypeScript的类型推断之处在于,它自然地鼓励声明式风格而不是命令式。创建一个对象并以命令式方式向其中添加内容需要你在空对象上放置一个过于宽松的类型,如Recordany,然后用兼容的值“填充它”。
例如,你示例的最直接转换是类似这样的:

// return type is `Record<string, any>`
export function createContext() {
  // type of `context` is locked in here
  const context: Record<string, any> = {};

  context.first = 'one';
  context.second = 2;
  context.third = new Date(3);

  return context;
}

函数推断出的返回类型始终是“返回的东西的类型”。
同样,如果你只是声明它为:

const context: any = {}

函数推断出的返回类型是any
如果你将其转换为声明式,TypeScript可以推断出上下文类型:

// type is:
// function(): {
//   first: string;
//   second: number;
//   third: Date;
// }
export function createContext() {
  return {
    first: 'one',
    second: 2,
    third: new Date(3),
  };
}

如果绝对需要命令式创建(通常不需要),你可以始终将字段作为值创建,然后返回整个声明式对象:

export function createContext() {
  const first = 'one';
  const second = 2;
  const third = new Date(3);

  return {
    first,
    second,
    third,
  };
}

这种转换纯粹是语法上的,所以不会很难做(尽管在没有一些使用正则表达式的巧妙查找/替换的情况下会很繁琐)。后两种情况都符合这个功能请求,因为在整个应用程序中,你可能不希望将其称为:

function someFunction(context: { first: string; second: number; third: Date }) { // ...

所以这将使得上述任何一个都可以轻松地声明为:

function createContext(): infer CoreContext {
  // ...
}

function someFunction(context: CoreContext) { //...

// or with destructuring:
function anotherFunction({ first, third }: CoreContext) { //...
mzillmmw

mzillmmw4#

我并不是想在这里自己给自己的特性找茬,但这是一个很大的范围扩展,因为你在那里使用的语法不是TS目前可以推断的东西。
是的,抱歉。现在TS只能在JS文件中推断它。在TS文件中这样做完全是不同的功能请求。我们在这里不要讨论它。
我应该准备一些没有这种语法的例子。我只是在看一些JS项目,它一直留在我的脑海里 😅

1szpjjfi

1szpjjfi5#

我想讨论在哪里可以使用 infer Type 语句,以及它的语义应该是什么样的。
对我来说,这个 infer Type 特性应该在它能做的事情上非常有限。我认为心智模型应该是:

  1. 它用 "Type" 替换 "infer Type"
  2. 在 "logical" 范围内声明带有推断类型的类型别名。
    以下是一些我想到的场景:
  3. 返回 o 函数
    这是 OP 描述的主要用例。在我看来最有用的。
//module1.ts
export function myFun() infer FunResult {
    return {
        foo: "",
        bar: 1
    }
}
export { FunResult }

我认为 infer 应该只在作用域(这里是模块的作用域)中引入别名。如果你想导出它,那么你需要手动导出它(export { FunResult })。这段代码基本上等同于:

export function myFun(): FunResult {
  return {
    foo: "",
    bar: 1,
  };
}
type FunResult = {
  foo: string;
  bar: number;
};
export { FunResult };
  1. 变量声明
    我们是否允许在变量声明中使用 infer Type?它会有多大用处?
function someFun() {
  return {
    foo: "",
    bar: 1,
  };
}

function otherFunction() {
  let t1: infer FunType = someFun();
  let t2: FunType = {
    foo: "f",
    bar: 1,
  };
}

这段代码等同于当前正在工作的代码:

function someFun() {
  return {
    foo: "",
    bar: 1,
  };
}

function otherFunction() {
  let t1: FunType = someFun();
  type FunType = {
    foo: string;
    bar: number;
  };
  let t2: FunType = {
    foo: "f",
    bar: 1,
  };
}

我不确定这种场景有多大用处。但这段代码的语义对我来说很清楚(我们已经在函数体中定义了类型别名,而且很明显这些类型仅限于函数体内部)

  1. "部分"推断(如 (infer T)[])
    就像条件类型一样,我希望 infer 能够“选择”一个类型的一部分(如 Type 来自 Array<Type>)
function someFun() {
  return [
    {
      foo: "a",
      bar: 1,
    },
    {
      foo: "b",
      bar: 2,
    },
  ];
}

function otherFunction() {
  let t1: (infer FunType)[] = someFun();
  let t2: FunType = {
    foo: "f",
    bar: 1,
  };
}

"otherFunction" 的主体等同于当前正在工作的代码:

function otherFunction() {
  let t1: FunType[] = someFun();
  type FunType = {
    foo: string;
    bar: number;
  };
  let t2: FunType = {
    foo: "f",
    bar: 1,
  };
}

总的来说,我认为 infer Type 这样的行为的清晰度和可理解性对于程序员来说应该是相当明显的。

ergxz8rk

ergxz8rk6#

我想讨论一下 infer Type 语句可以在哪里应用,以及它的语义应该是什么样的。
infer 关键字的语义已经存在于条件类型中:

type Result<T> = T extends Promise<infer R> ? R : T

虽然文档中没有正式的定义,但可能如下所示:

infer <Alias>:

Given an unnamed type that can be statically inferred, assign the reified type
to the supplied alias

语义保持不变。这只会改变语言 语法,以便在不同的地方使用它。
对我来说,这个 infer Type 功能应该在它能做什么方面非常有限。
它已经是这样了。这次更改只会添加一个额外允许的位置,那就是函数返回类型。
我认为心理模型应该是:
心理模型应该与 class 相同。剥离 ES6 类的语法糖,类实际上只是一个构造函数。TypeScript 提供了额外的语法糖来定义:

  • 一个值,即构造函数
  • 一个类型别名,即构造函数返回的示例类型

声明一个类可以访问这两个。然而,类是相当有限的,这将允许类似方便的 "创建者" 函数,它们要灵活得多。
我认为 infer 应该只在作用域(这里是模块的作用域)内引入别名。如果你想导出它,那么你需要手动导出它( export { FunResult } )。这段代码基本上等同于:
这完全抵消了其目的。调用者无法使用别名,只能使用匿名推断类型,因此在不导出类型别名的情况下导出函数与省略类型完全相同。
当你声明一个类时,值和类型总是共享一个词法范围。试想一下如果它们不是这样,而是需要显式的 export type ClassName

export class ExampleClass {
  constructor(
    public first: string,
    public second: number,
  )
}

// we forgot to export this commented line
// export type ExampleClass
// some-other-file.ts
import { ExampleClass } from './example'

// type of `example` would be an anonymous `{ first: string; second: number }`
const example = new ExampleClass('one', 2)

// and you couldn't explicitly refer to it, as this would be an error:
// Cannot find name 'ExampleClass'.ts (2304)
const example: ExampleClass = new ExampleClass('one', 2)
  1. 变量声明
    我们是否应该允许在变量声明中使用 infer Type?
    绝对不行。从概念上讲它是反转的。在赋值语句的“左手”进行推断的是变量的类型,而不是描述表达式结果的类型。“右手”表达式首先被评估,总是产生特定、明确类型的输出。即使它是匿名的,如 anyunknown ,它仍然是一个具有具体类型的值。如果你碰巧将其分配给一个变量,该变量的类型反映了该值的类型,而不是对其的定义。
let t1: FunType[] = someFun();
type FunType = {
  foo: string;
  bar: number;
};

在这里 someFun "拥有"类型。分配给其结果的任何变量都应该是那种类型。别名会产生混淆,而不是清晰度。唯一希望在消费者端有一个别名的原因是因为 someFun 是外部代码,缺少或错误地定义了类型。即使如此,你还是最好将函数 Package 在另一个定义更好类型的函数中:

type FunType = {
  foo: string;
  bar: number;
}

const typedFun = (): FunType[] => {
  return someFun()
}

然后将变量分配给那个“更正确的”类型:

// inferred type of `t1` is `FunType[]`
const t1 = typedFun();

相关问题