如何将构造函数选项对象参数类型化为强类型rest参数



我不知道如何在Typescript中输入这个。

我有一个Javascript类,我移植,简化在这里:

class A {
constructor ({
a = 'hi',
b = 5,
...rest
} = {}) {
this.a = a
this.b = b
this.extra = rest
}
}

构造函数接受带有一些默认值的options对象,并将其余的集合到一个对象中。该rest参数对象应该是平面,没有嵌套的对象或数组,并且需要JSON可序列化,并且是完全可选的。但是,如果存在,我希望为调用者提供一个机会,使其具有良好的类型。所以我定义了几个类型:

type KnownStuff = {
a: string
b: number
}
type FlatJSONCompatibleObject = {
[k: string]: string | number | boolean | null
} | {}

此外,我希望能够为其余部分指定一些额外的参数:

type Test = {
c: boolean
d: 'a' | 'b'
}

所以我尝试了以下操作:

class A <T extends FlatJSONCompatibleObject = {}> {
a: string
b: number
extra: T
constructor ({
a = 'hi',
b = 5,
...rest
}: Partial<KnownStuff> & T = {}) {
this.a = a
this.b = b
this.extra = rest // fails
}
}

哪个AFAICT失败,因为泛型没有约束。那么接下来:

class Foo <T extends Omit<FlatJSONCompatibleObject, keyof KnownStuff>> {
a: string
b: number
extra: T
constructor ({
a = 'hi',
b = 5,
...rest
}: Partial<KnownStuff> & T = {}) {
this.a = a
this.b = b
this.extra = rest
}
}

,但未能编译,我不能得到通用约束的正确。

我能做到的最接近的是:

class Bar<T extends Omit<FlatJSONCompatibleObject, keyof KnownStuff>> {
a: string
b: number
extra: Omit<FlatJSONCompatibleObject, keyof KnownStuff>
constructor ({
a = 'hi',
b = 5,
}: Partial<KnownStuff> = {}, rest: Partial<T> = {}) {
this.a = a
this.b = b
this.extra = rest
}
}

会编译,但会改变签名,我真的试图避免,因为这是为了工作,并且已经被多个其他团队广泛使用。这也是错误的:

const c = new Bar<Test>()

应该失败,因为参数上的属性不是可选的,但是我必须使用Partial<T>才能分配默认的空对象。我怀疑我完全搞错了,这就是为什么我问这个问题。

那么问题的约束条件:

  1. 类接受一个带有一些已知属性的options对象。
  2. 这些已知的属性应该都有默认值,new Whatever()应该工作。
  3. 类还可以接受对象中的一些附加属性。
  4. 额外的东西,如果存在必须是JSON兼容和平面(没有嵌套的对象或数组)。
  5. 类的用户应该能够传递一个类型参数,以便额外的东西有一个定义良好的编译器强制类型,即new Whatever<SomeTypeWithRequiredProperties>()应该工作。
  6. 我真的不想改变构造函数的签名(从Javascript POV),因为这已经被广泛使用。

如何输入?

游乐场,如果它有帮助

和一个(不正确,但希望给出要点)测试工具:

// class test harness
function test<T extends FlatJSONCompatibleObject = {}>(C: {new <U extends FlatJSONCompatibleObject = {}>(opts?: Partial<KnownStuff> & U): LP<T>}) {
// should work
const t1 = new C();
t1.extras // should be '{}'
const t2 = new C<{c: boolean}>({c: true});
t2.extras.c // should be true

// should be compile error
const t3 = new C<{c: boolean}>();
const t4 = new C<{c: boolean}>({d: 4});
}

我张贴这作为一个答案(而不是对问题的编辑),因为它至少部分回答了这个问题,但我不会接受它,因为a.我不完全理解为什么它的工作和b.我只能使它为函数工作,而不是一个类构造函数。感谢T.J. Crowder在评论中的帮助,我偶然发现了这个部分解决方案。

如果一个接口定义如下:

type LP<T extends FlatJSONCompatibleObject> = {
a: string
b: number
extras: T
}

,然后是像这样的重载函数:

function bar(): LP<{}>
function bar<T extends FlatJSONCompatibleObject>(opts: Partial<KnownStuff> & T): LP<T>
function bar(opts?: Partial<KnownStuff>): LP<{}>
{
const {
a = 'hi',
b = 5,
...extras
} = opts || {};
if (opts) {
return {
a,
b,
extras
}
} else {
return {
a,
b,
extras: {}
}
}
}
const bar1 = bar();
bar1.extras // {}
const bar2 = bar<{c: boolean}>({c: true})
bar2.extras.c // boolean
const bar3 = bar<{c: boolean}>() // compile error
const bar4 = bar<{c: boolean}>({d: boolean}) // compile error

这个可以工作,但是我不能让它为一个类工作:

class Foo <T extends FlatJSONCompatibleObject> {
public a: string
public b: number
// public extras: T | {}
public extras: T
constructor()
constructor(opts: Partial<KnownStuff> & T)
constructor(opts?: Partial<KnownStuff>)
{
const {
a = 'hi',
b = 5,
...extras
} = opts || {};
this.a = a
this.b = b
if (opts) {
this.extras = extras // error
} else {
this.extras = {} // error
}
}
}

虽然我可以更接近一点,但似乎通过显式地将额外参数设置为第二个参数:

class Bar<T extends FlatJSONCompatibleObject> {
public a: string
public b: number
public extras: T
constructor()
constructor(opts: Partial<KnownStuff>, extras: T)
constructor(opts?: Partial<KnownStuff>, extras?: never)
{
const {
a = 'hi',
b = 5.
} = opts || {}
this.a = a
this.b = b
if (extras) {
this.extras = extras
} else {
this.extras = {} // error!
}
}
}

我似乎仍然不能得到正确的过载。

游乐场

TL;DR

目前你还不能在100%类型安全的TypeScript中做到这一点。

细节:

我的TS之旅还相当早,但我认为这可能是一种情况:选择任何5(你的6).:-D我可以这样做,如果参数是可选的,但它不强制有一个参数,如果你提供一个类型参数与所需的属性(#1下面)。或者我可以在没有默认参数的情况下这样做(尽管我宁愿避免使用相对无害的@ts-ignore)(下面的第2条)。或者稍微扭转一下我之前的一个尝试,它非常接近(下面的第3条),但extra的类型是{} | x,而不仅仅是x。我询问了Titian Cernicova Dragomir,他确认你不能在今天的TypeScript中做到所有这些,并提供了下面的第4条,但像我的第1条一样,当你提供类型参数时,它也不会强制参数(同样,extra的类型最终会变成Partial<something>Partial<{}>)。

主要问题是您标记的:您不能将{}分配给泛型属性,因为该泛型的具体类型可能不允许{}

所以很遗憾,看起来要选择最接近的东西。

# 1

这个不能正确处理new A<something>():

操场上联系

type KnownStuff = {
a: string
b: number
}
type FlatJSONCompatibleObject = {
[k: string]: string | number | boolean | null
}
type Test = {
c: boolean
d: 'a' | 'b'
}
class A <Extra extends FlatJSONCompatibleObject = {}> {
a: string
b: number
extra: Extra
constructor ();
constructor (props: Partial<KnownStuff> & Extra);
constructor (props?: any) {
const {a = 'hi', b = 5, ...extra} = props ?? {}
this.a = a
this.b = b
this.extra = extra
}
}
const a1 = new A() // All good
a1.extra // Type is {}
const a2 = new A<{c: boolean}>({d: "hi"}) // Error as desired
a2.extra // Type is {c: boolean}
const a3 = new A<{c: boolean}>() // No error :-(
a3.extra // Type is {c: boolean}
const a4 = new A<{c: boolean}>({c: true}) // All good
a3.extra // Type is {c: boolean}

# 2

这是一个不允许向构造函数传递任何参数的类型:

操场上联系

type KnownStuff = {
a: string
b: number
}
type FlatJSONCompatibleObject = {
[k: string]: string | number | boolean | null
}
type Test = {
c: boolean
d: 'a' | 'b'
}
class A <Extra extends FlatJSONCompatibleObject = {}> {
a: string
b: number
extra: Extra
constructor ({a = 'hi', b = 5, ...extra}: Partial<KnownStuff> & Extra) {
this.a = a
this.b = b
// @ts-ignore
this.extra = extra
}
}
// No longer relevant
// const a1 = new A()
// a1.extra // Type is {}
const a2 = new A<{c: boolean}>({d: "hi"}); // Error as desired
a2.extra // Type is {c: boolean}
const a3 = new A<{c: boolean}>(); // Error as desired
a3.extra // Type is {c: boolean}
const a4 = new A<{c: boolean}>({c: true}); // All good
a3.extra // Type is {c: boolean}

# 3

这是你对extra,X | {}而不是X有一个尴尬的类型:

操场上联系

type KnownStuff = {
a: string
b: number
}
type FlatJSONCompatibleObject = {
[k: string]: string | number | boolean | null
}
type Test = {
c: boolean
d: 'a' | 'b'
}
class A <Extra extends FlatJSONCompatibleObject = {}> {
a: string
b: number
extra: Extra | {}
constructor (props?: Partial<KnownStuff> & Extra) {
if (props) {
const {a = 'hi', b = 5, ...extra} = props
this.a = a
this.b = b
this.extra = extra
} else {
this.a = 'hi'
this.b = 5
this.extra = {}
}
}
}
const a1 = new A() // All good
a1.extra // Type is {}
const a2 = new A<{c: boolean}>({d: "hi"}) // Error as desired
a2.extra // Type is {c: boolean} | {}
const a3 = new A<{c: boolean}>() // Error as desired
a3.extra // Type is {c: boolean} | {}
const a4 = new A<{c: boolean}>({c: true}) // All good
a3.extra // Type is {c: boolean} | {}

# 4

提香的Cernicova Dragomir有new A<something>()问题#1,extra的类型最终是Partial<{}>Partial<something>:

操场上联系

type KnownStuff = {
a: string
b: number
}
type FlatJSONCompatibleObject = {
[k: string]: string | number | boolean | null
} | {}
type Test = {
c: boolean
d: 'a' | 'b'
}
class A <T extends FlatJSONCompatibleObject = {}> {
a: string
b: number
extra: Partial<T>
constructor ({
a = 'hi',
b = 5,
...rest
}: Partial<KnownStuff> & Partial<T>  = {}) {
this.a = a
this.b = b
this.extra = rest as Partial<T>
}
}
const a1 = new A() // All good
a1.extra // Type is Partial<{}>
const a2 = new A<{c: boolean}>({d: "hi"}) // Error as desired
a2.extra // Type is Partial<{c: boolean}>
const a3 = new A<{c: boolean}>() // No error :-(
a3.extra // Type is Partial<{c: boolean}>
const a4 = new A<{c: boolean}>({c: true}) // All good
a3.extra // Type is Partial<{c: boolean}>

最新更新