-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Always use literal types #10676
Always use literal types #10676
Conversation
@mhegazy @RyanCavanaugh @DanielRosenwasser I'm sure you'll want to look at this! |
In your example function foo() {
return "hello";
}
const c1 = foo(); // string Why |
@Igorbek The issue is that you practically never want the literal type. After all, why write a function that promises to always return the same value? Also, if we infer a literal type this common pattern is broken: class Base {
getFoo() {
return 0; // Default result is 0
}
}
class Derived extends Base {
getFoo() {
// Compute and return a number
}
} If we inferred the type Of course, we could consider having different rules for functions vs. methods and only do the widening in methods, but I think that would be confusing. |
Should the expression initializing a |
This is great. I'm wondering if this will address the false positive scenario described by @AlexGalays in #6613 (comment). This is related to using f-bound polymorphism as a solution to "partial types" (e.g. for |
Yes, I think that would make sense.
No, the issue in #6613 is unrelated to this change. |
@ahejlsberg thank you, good point. However the same argument can be applied to conditional return as well: class Base {
startWithOne = true;
getFoo() {
return startWithOne ? 1 : 0; // now it will be typed as 1 | 0 ?
}
}
class Derived extends Base {
getFoo() {
// Compute and return a number
}
} Technically speaking, class methods cannot be considered immutable locations. And even plain functions cannot be: function foo() { return cond ? 1 : 2; }
foo = function() { return 3; } // error?
const foo2 = function() { return 1; } // now it's really immutable |
I would say that function declaration function foo() { return 1; } is equivalent (with hoisting in mind) to let foo = function () { return 1; }; where function () { return 1; }; // () => 1
let foo = function () { return 1; }; // () => number
function foo() { return 1; }; // () => number
const boo = function () { return 1; }; // () => 1 |
This is really great ❤️ ❤️ Just a small detail:
How about a function from which we want to return a finite set of literal types: function foo(n: 1 | 2 | 3) {
switch (n) {
case 1: return 'one';
case 2: return 'two';
case 3: return 'three';
default: return 'none';
}
} Although it's a better practice to explicitly type the return value, but this might a good example of inferring the return type as a literal. To avoid it, the developer can simply annotate the return type as string. But to make it return literal union another interface will need to be declared and maintained with the function body. |
@alitaheri The function in your example has a literal union type inferred as its return type: function foo(n: 1 | 2 | 3): "one" | "two" | "three" | "none"; Widening occurs only if the inferred return type is a singleton literal type. |
@Igorbek In the case of |
…ning literal types
I have updated the introduction to include the changes in the latest commits, plus I have added some more details and examples. |
Is there some kind of readonly array type I can use or write myself which will inform TypeScript that the array I am creating isn't allowed to be modified? Alternatively, is there some other trick I can use to get |
i am trying to understand this matter here, but i got some problem with terminology: quoting https://github.com/ahejlsberg The widened literal type of a type T is: But true or false are types itselfves? i don't get it... sorry if i'm annoying. |
This PR switches our approach to literal types from being a concept we sometimes use to a concept we consistently use. Previously we'd only use literal types in "literal type locations", which was not particularly intuitive. With this PR we always use literal types for literals and then widen those literal types as appropriate when they are inferred as types for mutable locations.
First some terminology:
true
,1
,"abc"
,undefined
.string
, if T is a widening string literal type,number
, if T is a widening numeric literal type,boolean
, if T istrue
orfalse
,E
, if T is a union enum member typeE.X
,Note that the widened literal type of any non-primitive type is simply the type itself. For example, the widened literal type of
{ kind: true }
is that type itself, even though the type contains a property with a literal type. Literal type widening is effectively a "shallow" operation.The following changes are implemented by this PR:
true
,1
,"abc"
).const
variable orreadonly
property without a type annotation is the type of the initializer as-is.let
variable,var
variable, parameter, or non-readonly property with an initializer and no type annotation is the widened literal type of the initializer.return
statements, the inferred return type is a union of the return expression types. Previously the return expressions were required to have a best common supertype, but this is no longer the case.The intuitive way to think of these rules is that immutable locations always have the most specific type inferred for them, whereas mutable locations have a widened type inferred. Some examples:
Literal type widening can be controlled through explicit type annotations. Specifically, when an expression of a literal type is inferred for a
const
location without a type annotation, thatconst
variable gets a widening literal type inferred. But when aconst
location has an explicit literal type annotation, theconst
variable gets a non-widening literal type.For further details on widening vs. non-widening string and numeric literals, see #11126.
In a function with no return type annotation, if the inferred return type is a literal type (but not a literal union type) and the function does not have a contextual type with a return type that includes literal types, the return type is widened to its widened literal type:
During type argument inference for a call expression the type inferred for a type parameter T is widened to its widened literal type if:
An occurrence of a type X within a type S is said to be top-level if S is X or if S is a union or intersection type and X occurs at top-level within S.
In cases where literal types are preserved in inferences for a type parameter (i.e. when no widening takes place), if all inferences are literal types or literal union types with the same base primitive type, the resulting inferred type is a union of the inferences. Otherwise, the inferred type is the common supertype of the inferences, and an error occurs if there is no such common supertype.
Some examples of type inference involving literal types:
Note that this PR fixes the often complained about issue of requiring a type annotation on a
const
to preserve a literal type for the const (e.g.const foo: "foo" = "foo"
) becauseconst
now consistently preserves the most specific type for the expression. However, the PR does not provide a way to infer literal types for mutable locations. That is an orthogonal issue we can address in a seperate PR if need be.NOTE: This description has been updated to reflect the changes introduced by #11126.