首页 > 解决方案 > 如何在 TypeScript 中将类型参数设置为“无”?

问题描述

在我们的应用程序中,我们有一个数据“包装器”模式,我们使用一个类的单个实例来表示一些可能尚未加载或尚未加载的数据。像这样的东西:

abstract class BaseWrapper<TData> {
    protected data?: TData;
    loadedPromise: Promise<this>;
}

然后,您将为特定的数据实现此功能,如下所示:

interface PersonData {
    name: string;
}

class PersonWrapper extends BaseWrapper<PersonData> {
    get name() { return this.data && this.data.name }
}

给定一个人:

const person: PersonWrapper = ...;

person.nameis string | undefinedsince的类型this.data可能未定义。这很好,因为它提醒我们对象可能没有被加载,我们需要在访问它的成员时处理这种情况。

但是,在某些情况下,我们知道对象已加载,最常见的loadedPromise是在完成时,如下所示:

(await person.loadedPromise).name

string | undefined即使我们知道对象已加载,该表达式的类型仍然是。这很烦人,我想帮助解决这个问题。

我最好的想法是BaseWrapper使用“空”类型参数进行参数化。像这样的东西:

abstract class BaseWrapper<TData, TEmpty = undefined> {
    protected _data?: TData;
    get data(): TData | TEmpty {
        return this._data as (TData | TEmpty);
    }
}

然后,我们可以这样做:

class PersonWrapper<TEmpty = undefined> extends BaseWrapper<PersonData, TEmpty> {
    get name() { return this.data && this.data.name }

    loadedPromise: Promise<PersonWrapper<nothing>>;
    requireLoaded(): PersonWrapper<nothing> {
        if (this.data) return this as PersonWrapper<nothing>
        else throw new Error("Not loaded!")
    }
}

这里的 issuer 是“nothing”类型。我希望能够将类型传递给参数,使得TData | TEmpty等于TData. 这种类型应该是什么?

假设这是可能的,那么类型(await person.loadedPromise).name就是string,这太棒了。

有任何想法吗?

标签: typescriptgenerics

解决方案


一种可能的解决方案是使用!后缀,它可以确保编译器有一个可选值可用:

const name: string = (await person.loadedPromise).name!

这适用于程序员“知道”该值已加载的非常明显的情况。


(嘿,我在重复你的“TData | TEmpty等于TData”的想法。你想要一种安全透明地封装可能的空虚的类型,即使你知道值已定义,也不需要你处理未定义的情况......)

另一个想法是:与其拥有namebe的类型,string | undefined为什么不拥有它... string[]!哈哈哈,忍我!

type Maybe<T> = T[] // I know, I know... this is weird

function of <T>(value: T): Maybe<T> {
  return value === undefined ? [] : [value]
}

abstract class BaseWrapper<TData> {
    protected data?: TData;
    loadedPromise: Promise<this>;
}

interface PersonData {
    name: string;
}

class PersonWrapper extends BaseWrapper<PersonData> {
    get name() { return of(this.data && this.data.name) }
}

const person: PersonWrapper = ...;

// these typings will be inferred I'm just putting them here to 
// emphasize the fact that these will both have the same type
const unloadedName: Maybe<string> = person.name
const loadedName: Maybe<string> = (await person.loadedPromise).name

现在,如果您想使用这些名称中的任何一个...

// reduce will either return just the name, or the default value
renderUI(unloadedName.reduce(name => name, 'defaultValue')
renderUI(loadedName.reduce(name => name, 'defaultValue')

Maybe类型表示一个可能未定义的值,并且它具有一个安全且一致的接口来访问它,而不管其状态如何。reduce 的默认值不是可选参数,所以不会忘记它:你总是要处理 undefined 的情况,这是非常透明的!如果您想进行更多处理,可以使用map.

unloadedName
  .map(name => name.toUpperCase)
  .map(name => leftPad(name, 3))
  .map(name => split('')) // <-- this actually changes the type
  .reduce(names => names.join('_'), 'W_A_T') // <-- now it's an array!

这意味着您可以编写如下函数,

function doStuffWithName(name: string) { 
  // TODO implement this 

  return name
}

并像这样称呼他们:

const name = person.name
  .map(doStuffWithName)
  .reduce(name => name, 'default')

renderUI(name)

并且您的辅助函数不需要知道它们正在处理可选值。由于 map 的工作方式,如果缺少该值,则namewill[]和 map 将应用给定函数 0 次......即。安全!

如果你对像这样重载数组类型感到不舒服,你总是可以Maybe作为一个类来实现:它只需要mapand reduce。你甚至可以在你的包装类上实现mapreduce(因为 Maybe 只是可能未定义数据的包装)。

这不是具有“无类型”的“特定于打字稿”的解决方案,但也许它会帮助您找到解决方案!

(注意:这很像暴露潜在的承诺,我认为这也是你可能要考虑的事情)


推荐阅读