Open DetachHead opened 3 years ago
And what is the benefit over using any
or unknown
?
You can't use unknown
because it's wider than the bound. and you shouldn't use any
because it then allows things from outside the bound.
@MartinJohns
if you use any
:
type Foo<T extends number> = T
declare function foo(value: Foo<any>): void
foo(1)
foo({a: "asdf"}) //no error???
if you use unknown
:
//error: Type 'unknown' does not satisfy the constraint 'number'.
declare function foo(value: Foo<unknown>): void
So basically just meaning "same as the constraint of the type argument", got it.
It has other usages in Kotlin, but those aren't applicable to Typescript due to the bivariance problem.
eg:
class Box<T>(var t: T)
fun foo(l: Box<*>) = print(l)
fun bar(l: Box<Any>) = print(l)
val box = Box(1)
foo(box) // no error
bar(box) // Box<Int> incompatible with Box<Any>
Currently I have to either type out the entire bound if it's not too bad, or I can make a type alias for the bound and use that instead. :(
Isnβt this just a form of existential types? i.e. Foo<*>
is roughly equivalent to βT: Foo<T>
. You can get close by using the constraint of T
for covariant types, but actual existential typing would be independent of variance.
Another reason why something like this would be nice is that you can't always just replicate the bound. Especially if the type is invariant in its type param, any
is your only choice currently.
type Foo<T> ={
x:T;
y: (z:T) => void;
}
function foo(x: Foo<*>): void {}
Neither unknown
nor never
works here, since Foo<number>
is neither a Foo<unknown>
nor. Foo<never>
If you want to avoid any
, you need to replicate the type param and make the function generic, which can be a pain in case of multiple type param and/or when the bounds have type arguments too
interface BlockModel {
}
interface Block<T extends BlockModel> {
type : string
}
interface ParagraphModel extends BlockModel {
}
interface Paragraph extends Block<ParagraphModel> {
}
interface ListModel extends BlockModel {
}
interface List extends Block<ListModel> {
}
class DefaultParagraph implements Paragraph {
type = "paragraph"
}
class DefaultList implements List {
type = "list"
}
type BlockType<K extends BlockModel,T extends Block<K>> = T
type Blocks = {
[K : string] : BlockType<*,*>
}
const blocks : Blocks = {
"paragraph" : new DefaultParagraph(),
"list" : new DefaultList()
}
Here's another example where this might be useful. Consider following code:
interface Box<T> {
getItem(): T;
}
interface BoxWrapper<T, B extends Box<T>> {
getBox(): B
}
interface BoxWrapperFactory {
create<T>(): BoxWrapper<T, Box<T>>;
}
class Something {
ping() {}
static fromBox(factory: BoxWrapperFactory) {
factory.create<Something>().getBox().getItem().ping();
}
}
We have to explicitly mention define Boxcreate<T>(): BoxWrapper<T, Box<T>>
or we can add default param to the BoxWrapper definition itself (that is, we can write interface BoxWrapper<T, B extends Box<T> = Box<T>> {
Both of constructions feels like slightly redundant. I can imagine that star syntax can come in handy.
Suggestion
π Search Terms
star projection
β Viability Checklist
My suggestion meets these guidelines:
β Suggestion
in kotlin, you can omit a generic from a type annotation when you don't care what its value is:
more info: https://typealias.com/guides/star-projections-and-how-they-work/
in typescript, to do the same thing you need to use
any
, which is gross:or simply duplicate the bound, which isn't convenient:
π Motivating Example
it's especially useful for types that have multiple bounded generics