You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TypeScript Version: 3.3.0, and the version on TS playground (3.4.1 as of this writing)
Search Terms: typeguard, empty string literal, undefined, union
Code
//--strictNullCheckstypeFalsy<T>=(|Extract<0,T>|Extract<(|false|null|undefined//Actually, only 0|NaN//But TS does not support NaN literal|number|""|0n|HTMLAllCollection),T>);declareconstemptyStrOrUndefined: Falsy<string|undefined>;declarefunctionisFalsy<T>(mixed: T): mixed is Falsy<T>;declareconsts: string|undefined;/* Hovering over this isFalsy() call gives me, function isFalsy<string | undefined>(mixed: string | undefined): mixed is "" | undefined This is as expected*/if(isFalsy(s)){//Expected: s should be ""|undefined now//Actual: s is undefinedif(s==undefined){//Should be undefined nowconsole.log(s);}else{//Expected: Should be "" now//Actual: s is `never`//Property 'length' does not exist on type 'never'.console.log(s.length);}}declarefunctionisEmptyStrOrUndefined(mixed: any): mixed is ""|undefined;if(isEmptyStrOrUndefined(s)){//Expected: s should be ""|undefined now//Actual: s is undefinedif(s==undefined){//Should be undefined nowconsole.log(s);}else{//Expected: Should be "" now//Actual: s is `never`//Property 'length' does not exist on type 'never'.console.log(s.length);}}
Expected behavior:
s should be ""|undefined where marked.
Also, unrelated, but this is where I would have liked a NaN literal in TS =/
This looks like a bug to me. The current behaviour when the source type is a union (here string | undefined) is that the source is filtered against the candidate type (here "" | undefined) removing all types not related to the candidate. The type string is not related to "" | undefined and is therefore filtered out, returning undefined (which is related to "" | undefined).
When the source type is not a union then the code intersects the source with the candidate (string & ("" | undefined)), producing the right narrowing.
I think getNarrowedType should probably map over unions first, applying narrowing to each constituent, abit like narrowTypeByTypeof.
We discussed this in a design meeting and decided there’s not currently a solution that’s worth the disruption it will bring. A hypothetical future where we can stop treating “branded primitives” as they exist today (e.g. string & { __kind: 'path' }) as possible value-containing types will help somewhat, and we also discussed this as an indicator that people often think about their object types as being closed, when in fact the type system doesn’t represent this at all (aside from excess property checking on fresh object literals).
I may look into doing the correct thing when all constituents of both the original type and the predicate type are primitives, since intersections of those are more intuitive and fall out when empty. On the other hand, I’m not sure if we want that kind of logical branching—maybe it’s better just to keep it in its current slightly wrong but consistent state. At any rate, this PR is out of scope for that kind of change.
//--strictNullChecksdeclareconsts: string|undefined;declarefunctionisEmptyStrOrUndefined(mixed: string|undefined): mixed is ""|undefined;if(isEmptyStrOrUndefined(s)){//Expected: s should be ""|undefined now//Actual: s is undefinedif(s==undefined){//Should be undefined nowconsole.log(s);}else{//Expected: Should be "" now//Actual: s is `never`//Property 'length' does not exist on type 'never'.console.log(s.length);}}declarefunctionisEmptyStr(mixed: string|undefined): mixed is "";declarefunctionisUndefined(mixed: string|undefined): mixed is undefined;if(isEmptyStr(s)||isUndefined(s)){//Expected: s should be ""|undefined now//Actual: s is ""|undefined; as expectedif(s==undefined){//Should be undefined nowconsole.log(s);}else{//Expected: Should be "" now//Actual: s is ""; as expectedconsole.log(s.length);}}
src/index.ts:48:7 - error TS2367: This condition will always return 'false' since the types 'AST_NODE_TYPES.Literal' and 'AST_NODE_TYPES.TemplateLiteral' have no overlap.
48 if (argument.type === AST_NODE_TYPES.TemplateLiteral) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The change that caused this issue is that StringLiteral defines value as type string, where as in the past it was type boolean | number | RegExp | string | null (which is what it is defined as on its super class LiteralBase, which hasn't changed).
If I comment out the value: string on StringLiteral, it all just works.
I ran into this in real-world code. This is an abbreviated reproduction of my case:
// This is necessary for the issue to occur
interface PrimitiveType {
kind: "string"
}
interface ArrayColumnType {
kind: "array";
items: PrimitiveType | TableRefType;
}
// MultiRelationType is a specialized version of ArrayColumnType
interface MultiRelationType {
kind: "array";
items: TableRefType;
}
interface TableRefType {
kind: "table-ref";
tableName: string;
}
type ColumnType = PrimitiveType | ArrayColumnType | TableRefType;
export function isRelationType(t: ColumnType): t is (TableRefType | MultiRelationType) {
return t.kind === "table-ref" || (t.kind === "array" && t.items.kind === "table-ref");
}
function makeColumnType(): ColumnType {
return { kind: "array", items: { kind: "table-ref", tableName: "foo"}};
}
const foo = makeColumnType();
// Here foo can be one of PrimitiveType | ArrayColumnType | TableRefType.
if (isRelationType(foo)) {
// Now we know foo is one of TableRefType | MultiRelationType.
// But TS seems to think that foo is definitely a TableRefType:
console.log(foo.tableName); // no error
}
Activity
jack-williams commentedon Apr 29, 2019
This looks like a bug to me. The current behaviour when the source type is a union (here
string | undefined) is that the source is filtered against the candidate type (here"" | undefined) removing all types not related to the candidate. The typestringis not related to"" | undefinedand is therefore filtered out, returningundefined(which is related to"" | undefined).When the source type is not a union then the code intersects the source with the candidate (
string & ("" | undefined)), producing the right narrowing.I think
getNarrowedTypeshould probably map over unions first, applying narrowing to each constituent, abit likenarrowTypeByTypeof.RyanCavanaugh commentedon Apr 29, 2019
Andrew Branch (@andrewbranch) fyi I didn't look fully into this, but generally assume Jack Williams (@jack-williams) is right 😉
andrewbranch commentedon May 20, 2019
Ryan Cavanaugh (@RyanCavanaugh) given the discussion in #31206, what should we bump the milestone to?
andrewbranch commentedon Jun 24, 2019
Cross-posting from PR #31206 for visibility:
AnyhowStep commentedon Jun 24, 2019
I need a sad face react =(
Thank you for the update, and the attempt!
AnyhowStep commentedon Dec 31, 2019
This is a repro of a problem Gareth Jones (@G-Rath) had, on TS 3.7.4,
Playground
It seems like type guard unions heck things up.
Gareth Jones (@G-Rath) 's example is more complex and involves the
ts-estreepackage, though.Is this because it's checking subtypes only when the type guard is for a union type?
And it should be checking both subtypes and supertypes?
[EDIT]
It seems like his problem is different from mine. See his comment 👇 for more info.
The workaround of not using union type guards works, though, even if cumbersome.
Instead of,
Use,
G-Rath commentedon Dec 31, 2019
This is my reproduction repo.
I came across this when upgrading @typescript-eslint/typescript-estree from 2.12.0 to 2.13.0 as part of package update on
eslint-plugin-jest.The change that caused this issue is that
StringLiteraldefinesvalueas typestring, where as in the past it was typeboolean | number | RegExp | string | null(which is what it is defined as on its super classLiteralBase, which hasn't changed).If I comment out the
value: stringonStringLiteral, it all just works.Here's a playground helpfully supplied by AnyhowStep
schani commentedon Nov 29, 2020
I ran into this in real-world code. This is an abbreviated reproduction of my case:
octogonz commentedon Dec 8, 2020
We encountered a vaguely similar problem in #41871. The commonalities seem to be:
Our repro works all the way back to TypeScript 2.0.10, so this problem apparently has been around for a while.
DetachHead commentedon Apr 26, 2021
minimal example
jtbandes commentedon Apr 28, 2021
I also ran into this problem:
I'm going to work around it for now by returning
string | undefinedrather than trying to use it as a type guard.