Search code examples
typescript

How to declare a variable and populate it using a custom type in Typescript


This is driving me crazy.

I have this custom type:

export default interface Folder {
    name: string;
    reports: Array<Report>;
}

I want to build an array of Folders stored in:

reportsDashboard: [] as Folder[]

I retrieve a list of reports against a folder. I have the folder name and I have a list of report of type Report to iterate through.

export default interface Report {
    datasetId: string;
    datasetWorkspaceId: string;
    embedUrl: string;
    id: string;
    isFromPbix: boolean;
    isOwnedByMe: boolean;
    name: string;
    reportType: string;
    subscriptions: Array<any>;
    users: Array<any>;
    webUrl: string;
    folder: string | undefined;
}

First I need to declare a variable to populate my folder, then I want to push that into my reportsDashboard array.

And folderName is a string | undefined:

const folderName: string | undefined = reportsStore.reportWorkspaceMap.find((w) => w.role === roles[role])?.folder;

Here's the attempt that led me here...

// This is what the AI tried to tell me would work HA!

let folder: Folder | null = null;
folder.name = folderName;

for (const report in incomingReports) {
    folder.reports.push(r);
}

This is what is now working when I added | undefined to the name property in my Folder type

// I started with this somewhat, but I guess the undefined threw me off, thanks @Phil

let folder: Folder = { name: folderName, reports: [] };
folder.name = folderName;

for (const report in incomingReports) {
    folder.reports.push(r);
}

With setting folder to null I got the IDE and Console messages:

// IDE: "'folder' is possibly 'null'"
// Console: "TypeError: Cannot set properties of null (setting 'name')"

With not setting folder to null I get the IDE and Console messages:

// IDE: "Type 'string | undefined' is not assignable to type 'string'.
//          Type 'undefined' is not assignable to type 'string'."
// Console: "TypeError: Cannot set properties of undefined (setting 'name')"

I couldn't find a simple example so I tried Open AI, it's worked in the past. I even asked it how to deal with these specific errors and in all ways I asked it said that setting it to null should tell TypeScript that "this is expected, it's ok"... but no.

And to top it off, in the second example, when I try to push the report into my array, I get this (and still do, but it works):

Argument of type 'import("c:/Projects/Project/types/Report").default' is not assignable to parameter of type 'Report'.
  Type 'Report' is missing the following properties from type 'Report': body, type, url, toJSON

EDIT 1

And here I was thinking it was obvious.

In JavaScript I would (paraphrasing for brevity in a way, and because in my actual app there's a couple of API calls getting stuff from Power BI etc.):

const workspaces = [
    {
        name: "Workspace 1",
        id: "some guid"
    },
    {
        name: "Workspace 2",
        id: "some guid"
    },
]

const incomingReports = [
    {
        name: "Report 1",
        url: "Url"
    },
    {
        name: "Report 2",
        url: "Url"
    },
]

for (const workspace in workspaces) {
    const folderName = workspace.name
    // fetch reports for workspace resulting in incomingReports
    let folder = { name: folderName, reports: []}
    for (const r in incomingReports) {
        folder.reports.push(r)
    }
}

Should result in a set of folder objects placed into another array where the folder object looks like:

{
    name: "Workspace 1",
    reports: [
        {
            name: "Report 1",
            url: "Url"
        },
        {
            name: "Report 2",
            url: "Url"
        },
    ]
}

I'm stuck on populating a variable of type Folder is all.

I guess I'm not understanding how you create a variable of a type then give it values without TS telling me it doesn't have values. Clearly that's obvious, especially with the code suggested above.

All the examples I've found to learn this have been too simple, too complex, or worked for 'whoever I was watching or reading' at the time.

EDIT 2

When I don't specify the type:

enter image description here

r is Argument of type 'Report' is not assignable to parameter of type 'never'.

When I do specify the type:

enter image description here

name is Type 'string | undefined' is not assignable to type 'string'.

r is Type 'Report' is missing the following properties from type 'Report': body, type, url, etc

EDIT 3

So I got it working, kind of.

Updated Folder type to (added | undefined) which feels dirty:

export default interface Folder {
    name: string | undefined;
    reports: Array<Report>;
}

But the r variable is still yelling, but the data populates as expected. I guess it's now the way I'm defining the reports array there.

I tried updating the Name type earlier to include undefined, but I think my code was slightly different so TS warned me of something else.


Solution

  • Those error messages are telling you what is wrong.

    let folder: Folder | null = null;
    folder.name = folderName;
    

    here you are saying folder is a variable of type Folder, or it is a null object.

    Then you set it to be a null object. This is allowed, as the types match.

    A null object has no properties. So when you type folder.name you are possibly accessing an object with no properties. TypeScript points out this error.

    The easiest way to fix this is to not make folder possibly a null object.

    When you try to fix this, you get an error on:

    folder.name = folderName;
    

    Now you have attempted to make this hard for people to solve by hiding what type of value folderName is. Unfortunately for you, type script errors explain what your problem is very clearly.

    It states you are trying to assign something of type string | null to a variable of type string.

    TypeScript says you can't do this: a name is a string and guaranteed to be non-null. Meanwhile the value on the right explicitly states it can be null.

    So TypeScript gives you an error.

    Types in languages move runtime errors to compile time. The logic error - that the string could end up null when you promised it would never be - becomes a compile time error in typescript, while in JavaScript you'd have to catch it at runtime.

    Or, more realistically, you'd fail to catch it, you'd ship the code, it would crash 1 in 1000 times when it got null input starting 1 month later, and each of those crashes would break an average value 10$ transaction. At 1 million transactions per hour and it taking 5 hours to find, fix, verify, and deploy the fix, this costs the company 50,000$ in lost revenue.

    Which is why they are making you use TypeScript so you fix this bug at compile time instead of shipping buggy code.

    To understand TypeScript you'll want to understand sum and product types (or union and intersections). https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types is one source of docs.

    The easiest is just

    if(typeof folderName == 'string') {
      folder.name = folderName;
    } else {
    

    Then write how you should handle it if a null folderName was passed in. Set folder.name = 'i am a chicken'? Whatever you want.