如何在TypeScript中将类型参数设置为“无”?

时间:2018-06-21 21:39:53

标签: typescript generics

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

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.name的类型为string | undefined,因为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!")
    }
}

此处的发行者为“无”类型。我希望能够将类型传递给参数,以使TData | TEmpty等于TData。这种类型应该是什么?

假设这是可能的,(await person.loadedPromise).name的类型将仅为string,这将非常棒。

有什么想法吗?

1 个答案:

答案 0 :(得分:1)

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

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

这在程序员“知道”该值已加载的非常明显的情况下有效。


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

另一个想法是:与其将name的类型设为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的默认值不是可选参数,因此没有机会忘记它:您总是必须处理未定义的情况,这非常透明!如果您想进行更多处理,可以使用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的工作方式,如果缺少值name将为[],并且map将应用给定的函数0次...即。安全!

如果您不喜欢这样重载数组类型,则始终可以将Maybe作为一个类来实现:它只需要mapreduce。您甚至可以在包装类上实现mapreduce(因为Maybe只是包装可能未定义的数据)。

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

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