-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Type checking element typed arrays #16389
Comments
Yeah, might be nicer if it'd just infer type |
But how on earth can you guarantee that nothing called |
literal types are only inferred, in mutable locations, if there is a contextual type. this is mainly a backward compatibility guarantee. |
Hm. If |
It is actually more strict, as you could be assigning |
Yeah it would be nice if I could strictly define the array as a tuple so that the compiler can be sure I haven't pushed to it, etc. It'd be good because then people could choose to use it as the way they default create arrays, same way generally you default to const for variable decls. I.e. Have a tuple array construct which forces the compiler to treat the array as truly immutable (but still emit a normal js array). |
Well you can do You could always spin your own definitely typed class, I suppose, though I don't know for sure how it would exactly work. Seems like it would be doable. A tuple syntax or marker would be really nice so we don't have to declare the type and the value separately. Makes it really handy for chaining promises and observables. |
Even if defaulting to granular types (tuples, string/number/boolean literals) would somehow break some edge cases, perhaps using a compiler flag like for |
It would break most code, because most of the time you don't think about it, but any literal that is not a constant is predicted to change in value, though not in type. An Array literal might not be as likely to, but it would probably still break a lot of code. But adding new syntax wouldn't break anything. |
Edit: Sounds like a candidate for a compiler flag. Libraries won't suffer much, as their types will get distributed as .d.ts, making them immune to the user's compiler flags. |
No, array push is definitely allowed on const. This is due to the intent of const, where a variable's pointer is declared readonly, so you know you will never accidently assign a different value to (for instance) the NodeJS Path module and you will always be using the same object reference. |
If you want a definite readonly or frozen object, it is not hard to spin your own. Some of the functionality is already included in browsers by default ( |
Thanks. I won't argue against your own use case of explicit per-case opt-in; I think both sound legit. If you'd wanna use Ramda's |
I'm not sure a blanket flag would be the best idea, as it would mean you would be severely break from standard JS syntax. Maybe a new keyword (which compiles to You could then also apply this keyword to methods to signify that a function is pure. Which is how the compiler could tell what functions on an object are safe to call for a This keyword could be enforced by the compiler, meaning that a function marked with the pure keyword cannot have a body which: Additionally the compiler could automatically mark methods as |
@bradzacher: sounds cool to me, but I fear detecting function purity is far off... callbacks, Promises and dynamic imports probably aren't helping. |
I hadn't initially understood this from mhegazy's response, but it appears string literals already discriminate between mutable locations (e.g. const foo = 'foo'; // 'foo'
let bar = 'bar'; // string So it appears that arrays (and objects) are still handled differently in that regard. |
Yes, but a global flag would never work, because there are use cases where an array is declared with an element in it and then added to. Since this is perfectly valid Javascript, I can hardly see this flag working. An array is an array, and if we want to declare a Tuple we will have to use something different at that line of code. Something like this works (almost). function newTuple<T0>(item0: T0): [T0];
function newTuple<T0, T1>(item0: T0, item1: T1): [T0, T1];
function newTuple<T0, T1, T2>(item0: T0, item1: T1, item2: T2): [T0, T1, T2];
function newTuple<T0, T1, T2, T3>(item0: T0, item1: T1, item2: T2, tem3: T3): [T0, T1, T2, T3];
function newTuple<T0, T1, T2, T3, T4>(item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4): [T0, T1, T2, T3, T4];
function newTuple<T0, T1, T2, T3, T4, T5>(item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4, item5: T5): [T0, T1, T2, T3, T4, T5];
function newTuple<T0, T1, T2, T3, T4, T5, T6>(item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4, item5: T5, item6: T6): [T0, T1, T2, T3, T4, T5, T6];
function newTuple<T0, T1, T2, T3, T4, T5, T6, T7>(item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4, item5: T5, item6: T6, item7: T7): [T0, T1, T2, T3, T4, T5, T6, T7];
function newTuple<T0, T1, T2, T3, T4, T5, T6, T7, T8>(item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4, item5: T5, item6: T6, item7: T7, item8: T8): [T0, T1, T2, T3, T4, T5, T6, T7, T8];
function newTuple<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9>(item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4, item5: T5, item6: T6, item7: T7, item8: T8, item9: T9): [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9];
function newTuple<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, TR>(item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4, item5: T5, item6: T6, item7: T7, item8: T8, item9: T9, rest: TR): [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9] & TR;
function newTuple(...args: any[]): any {
return args;
} The only thing that doesn't work is |
Here is a small update that allows chaining the value, but shows the problem with chaining the type. function newTuple<T0>(item0: T0): [T0];
function newTuple<T0, T1>(item0: T0, item1: T1): [T0, T1];
function newTuple<T0, T1, T2>(item0: T0, item1: T1, item2: T2): [T0, T1, T2];
function newTuple<T0, T1, T2, T3>(item0: T0, item1: T1, item2: T2, tem3: T3): [T0, T1, T2, T3];
function newTuple<T0, T1, T2, T3, T4>(item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4): [T0, T1, T2, T3, T4];
function newTuple<T0, T1, T2, T3, T4, T5>(item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4, item5: T5): [T0, T1, T2, T3, T4, T5];
function newTuple<T0, T1, T2, T3, T4, T5, T6>(item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4, item5: T5, item6: T6): [T0, T1, T2, T3, T4, T5, T6];
function newTuple<T0, T1, T2, T3, T4, T5, T6, T7>(item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4, item5: T5, item6: T6, item7: T7): [T0, T1, T2, T3, T4, T5, T6, T7];
function newTuple<T0, T1, T2, T3, T4, T5, T6, T7, T8>(item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4, item5: T5, item6: T6, item7: T7, item8: T8): [T0, T1, T2, T3, T4, T5, T6, T7, T8];
function newTuple<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9>(item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4, item5: T5, item6: T6, item7: T7, item8: T8, item9: T9): [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9];
function newTuple<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, TR>(item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4, item5: T5, item6: T6, item7: T7, item8: T8, item9: T9, restTuple: TR): [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9] & TR;
function newTuple(...args: any[]): any {
//it will never be more than one deep because function arguments are resolved
//before the function call
if (args.length === 11) {
return args.concat(args.pop());
} else {
return args;
}
}
const test = newTuple("", "", "", "", "", "", "", "", "", "", newTuple("", 0, "", "test" as "test", "test"));
var t0 = test[0]; // type: string
var t1 = test[1]; // type: string & number
var t2 = test[2]; // type: string
var t3 = test[3]; // type: string & "test"
var t4 = test[4]; // type: string
var t5 = test[5]; // type: string
var t6 = test[6]; // type: string
var t7 = test[7]; // type: string
var t8 = test[8]; // type: string
var t9 = test[9]; // type: string
var t10 = test[10]; // type: string | (string & number)
var t11 = test[11]; // type: string | (string & number)
var t12 = test[12]; // type: string | (string & number)
var t13 = test[13]; // type: string | (string & number)
var t14 = test[14]; // type: string | (string & number)
var t15 = test[15]; // type: string | (string & number)
var t16 = test[16]; // type: string | (string & number)
var t17 = test[17]; // type: string | (string & number)
// repeats t17 on up. |
Or using a constructor interface interface TupleConstructor {
new <T0>(item0: T0)
: [T0];
new <T0, T1>(item0: T0, item1: T1)
: [T0, T1];
new <T0, T1, T2>(item0: T0, item1: T1, item2: T2)
: [T0, T1, T2];
new <T0, T1, T2, T3>(item0: T0, item1: T1, item2: T2, tem3: T3)
: [T0, T1, T2, T3];
new <T0, T1, T2, T3, T4>(item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4)
: [T0, T1, T2, T3, T4];
new <T0, T1, T2, T3, T4, T5>(item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4, item5: T5)
: [T0, T1, T2, T3, T4, T5];
new <T0, T1, T2, T3, T4, T5, T6>(
item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4, item5: T5, item6: T6)
: [T0, T1, T2, T3, T4, T5, T6];
new <T0, T1, T2, T3, T4, T5, T6, T7>(
item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4, item5: T5, item6: T6, item7: T7)
: [T0, T1, T2, T3, T4, T5, T6, T7];
new <T0, T1, T2, T3, T4, T5, T6, T7, T8>(
item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4, item5: T5, item6: T6, item7: T7, item8: T8)
: [T0, T1, T2, T3, T4, T5, T6, T7, T8];
new <T0, T1, T2, T3, T4, T5, T6, T7, T8, T9>(
item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4, item5: T5, item6: T6, item7: T7, item8: T8, item9: T9)
: [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9];
new <T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, TR extends any[]>(
item0: T0, item1: T1, item2: T2, tem3: T3, item4: T4, item5: T5, item6: T6, item7: T7, item8: T8, item9: T9, restTuple: TR
): [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9] & TR;
}
const Tuple: TupleConstructor = (((...args: any[]) => {
//it will never be more than one deep because function arguments are resolved
//before the function call
if (args.length === 11) {
return args.concat(args.pop());
} else {
return args;
}
}) as any) as TupleConstructor; |
I only have 4 years of experience in JavaScript, and 6 months in Typescript, so if more experienced users want to weigh in on this, please do. |
@Arlen22: experience-wise that sounds fine to me; JS only got good from ES6 anyway. :D If you have a nice solution for the object case, it might yield you a bounty on SO. That said, your approach doesn't appear recursive -- the ideal inferred contents, for both heterogeneous arrays (= tuples) and objects would still be granular (e.g. string literals), or it'd fail for the purpose of e.g. properly inferring navigation. It should stay as granular as possible until forced out of it by expected mutation.
I'm not saying there's be no drawback, otherwise it shouldn't need a flag. The limitation is pretty easy to deal with though: if you must use mutation on a variable, use Alternatively, the behavior could be relegated to a new keyword, in which case a flag would no longer be needed. But those interested in granular inference seem more likely to favor a functional approach over mutation (precise inference matters most there), and if they had to, presumably may not mind a |
@Arlenn22: yeah, looks like methods like
Obviously this is super hard with static type systems while at run-time things can change all over the place, but yeah. At least for the case of objects, I'm not so sure the degeneration is aiding for backward compatibility concerns like in the Edit: if the following is considered legitimate, then we may have a solution to the const x = [90210];
x.push('Beverly Hills'); |
The way Javascript works is that you have 4 primitives: string, bool, number, and reference (am I missing one). A reference refers to any object (whether an Array or Hashmap). A variable gets assigned a primitive (whether |
You can use However, the new type must be assigned to a new variable. The type of a variable cannot be changed once it is assigned. For this reason, I use interfaces to specify the eventual properties of a variable. Depending on how I use it, I may or may not make it optional. If I am generating an object and returning it, I usually will not make it optional. interface IMy {
hello: string;
world: string;
}
const me: IMy = {} as any; //as any will prevent an error
IMy.hello = "test"; |
I'll admit it's a tough problem. Rather than the flag, I wonder if an alternative solution might be to add an overload for tuple interfaces for e.g. Edit: I further worked this out into concrete tuple overloads at #16656 (comment). |
I think my proposed changes here might also help address issues raised in #212 (comment) w.r.t. typing |
Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed. |
TypeScript Version: 2.3.4
Code
Expected behavior:
calling case TWO should pass the type check fine
Actual behavior:
case TWO throws type check error:
Argument of type 'string[]' is not assignable to parameter of type '("a" | "b")[]'. Type 'string' is not assignable to type '"a" | "b"'.
typescript automatically casts the variable
x
to typestring[]
, and in doing so loses all information about its contents, which is why it fails the strict value check.this is a problem because it means you can't pass reusable arrays without strictly typing the original variable, which causes problems when using generics (a bit of a contrived example, but still):
The text was updated successfully, but these errors were encountered: