microsoft / TypeScript

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

Infer typeof self if unspecified on cast #45618

Open alita-moore opened 3 years ago

alita-moore commented 3 years ago

Suggestion

πŸ” Search Terms

βœ… Viability Checklist

My suggestion meets these guidelines:

⭐ Suggestion

It would be a lot cleaner if when you cast a type to some other type (for example NonNullable) you don't have to specify typeof self.

πŸ“ƒ Motivating Example

if you have an Array that could be undefined you can filter out undefined like so

const array = ["one", undefined, "two"].filter(Boolean);
console.log(array) // ["one", "two"]

but in this case the type would still be (string | undefined)[] so it would cause linting errors later in the code. While this may be specific to the .filter method, it may be unreasonable to expect all methods accurately account for type checks like this. So one easy way to fix this would just be to do

const arrayUndefined = ["one", undefined, "two"]

const array = arrayUndefined.filter(Boolean) as NonNullable<UnArrayify<typeof arrayUndefined>>

what I'm suggesting is that instead of requiring you do typeof arrayUndefined you can instead do an implicit cast to make the code cleaner

const arrayUndefined = ["one", undefined, "two"]

// it's implied that UnArrayify would be called with <typeof array.filter(Boolean)>
const array = arrayUndefined.filter(Boolean) as NonNullable<UnArrayify>

πŸ’» Use Cases

I would like this because it would result in cleaner typing for unhandled edgecases in the typescript linter

MartinJohns commented 3 years ago

would result in cleaner typing

The clean approach would be to use a type guard and avoid the type assertion (aka "cast") entirely.

Perhaps you can provide a better example?

Also you checked that this wouldn't be a breaking change, but introducing a new global type could very well be breaking for anyone having such a type defined.

alita-moore commented 3 years ago

@MartinJohns what do you mean by global type? I guess I don't know how this would be implemented from the Typescript side. But I don't know how it would involve a global type.

What do you mean by type guard? Do you mind providing an example

I.e. All current code would already be breaking types if they cast to a typescript function (what do you call them?) that had a required type. In that case it wouldn't infer self, but in the case where you cast to a type with a required type but don't define that type it would cast. So at least assuming the code is syntactically correct right now it would break.

alita-moore commented 3 years ago

here's a couple examples that may help explain what I mean

type CastTo<T> = T

const array = ["string"]

// currently..
const str = array[0] as CastTo<number>;
str -> number
const str = array[0] as CastTo; // expected required type T

// I propose
const str = array[0] as CastTo<number>;
str -> number
const str = array[0] as CastTo; // i.e. CastTo is seen as CastTo<typeof array[0]> to the linter
str -> string
alita-moore commented 3 years ago

also it's not always possible to define type as non-nullable without running a potentially null array through a filter to assure the contents are defined.

I think the main reason this may cause issues right now though is because people might not notice that the type their casting to is being called with the implicit type which would be confusing. So maybe this isn't a great idea πŸ€·β€β™€οΈ

in this case considering what I just mentioned it would be better to just have an implicit type available called self.. but now I see what you meant by a global type.. 😒

MartinJohns commented 3 years ago

What do you mean by type guard? Do you mind providing an example

function isNotUndefined<T>(value: T | undefined): value is T {
  return value !== undefined;
}

const array1 = ["one", undefined, "two"].filter(Boolean); // type is (string | undefined)[]
const array2 = ["one", undefined, "two"].filter(isNotUndefined); // type is string[]

No type-assertion needed. Every type-assertion is a potential issue, because you overwrite the type-system and tell the compiler "shh... trust me, it's this type" (even if this may not be the case).

jcalz commented 3 years ago

As proposed, this would be a breaking change for any generic type with a type parameter default. For example, given type Foo<T = string> = ..., writing 123 as Foo is currently the same as writing 123 as Foo<string>, but you are suggesting it should be interpreted as 123 as Foo<123> instead?

alita-moore commented 3 years ago

@jcalz it would only apply to required types, Foo<T = string> is an optional type. So it wouldn't affect those as proposed.

alita-moore commented 3 years ago

Also I encountered another use case where I want to make a type mutable (using type-fest) but I have to first define the const and then redfine it to get that type.

My exact example is

const _context = (await import("github")).context
const context = _context as Mutable<type of _context>

// OR
const context = (await import("github")).context as Mutable<PromiseValue<ReturnType<typeof import("github")>>["context"]>

So my proposed change would instead be this

const context = (await import("github")).context as Mutable

Which is essentially just option 2 but implicit. This implicit type could be called self or something similar as well if you'd like it to be more transparent / control. I.e.

const context = (await import("github")).context as Mutable<self>

Notably self would only be defined in this context similar to how dynamic types are defined in functions and so it would only interfere with other types if you wanted to use anther type called self in a cast. But this seems to me to be a negligible edgecase.

Generally speaking this would clean up a lot of my code

MartinJohns commented 3 years ago

You can achieve something like that trivially using a helper function without weird and confusing type shenanigans:

function asMutable<T>(value: T): Mutable<T> { return value; }

const context = asMutable(await import("github")).context);
alita-moore commented 3 years ago

You can achieve something like that trivially using a helper function without weird and confusing type shenanigans:

function asMutable<T>(value: T): Mutable<T> { return value; }

const context = asMutable(await import("github")).context);

Yeah but it's still adding complexity to my code. Which defeats the purpose

alita-moore commented 1 year ago

Checking in on this, I still think this would be a helpful addition.