microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.59k stars 12.44k forks source link

support "star projections" in generics #46532

Open DetachHead opened 2 years ago

DetachHead commented 2 years ago

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:

class Foo<T: Number>

fun foo(value: Foo<*>) {}

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:

type Foo<T extends number> = T

declare function foo(value: Foo<any>): void

or simply duplicate the bound, which isn't convenient:

type Foo<T extends number | string | boolean> = T

declare function foo(value: Foo<number | string | boolean>): void

πŸ“ƒ Motivating Example

it's especially useful for types that have multiple bounded generics

type ThingWithLotsOfGenerics<Type1 extends Base1, Type2 extends Base2, Type3 extends Base3> = {}

declare function foo(value: ThingWithLotsOfGenerics<*, *, *>): void
MartinJohns commented 2 years ago

And what is the benefit over using any or unknown?

KotlinIsland commented 2 years ago

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.

DetachHead commented 2 years ago

@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
MartinJohns commented 2 years ago

So basically just meaning "same as the constraint of the type argument", got it.

KotlinIsland commented 2 years ago

It has other usages in Kotlin, but those aren't applicable to Typescript due to the bivariance problem.

KotlinIsland commented 2 years ago

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>
mchccn commented 2 years ago

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. :(

fatcerberus commented 2 years ago

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.

blutorange commented 2 years ago

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

wakaztahir commented 1 year ago
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()
}
Schahen commented 11 months ago

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 Box either like it's done in code, at line create<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.